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_parameter_name, python_plain_string_literal,
6};
7use brokk_bifrost_core::analyzer::fq_name::{FqName, SegmentKind, segment_interner};
8use brokk_bifrost_core::analyzer::model::{
9 CallableArity, 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::project::Project;
16use brokk_bifrost_core::analyzer::python_facts::{PythonCallableReturnFact, PythonSourceFacts};
17use brokk_bifrost_core::analyzer::resolution_facts::ResolutionSiteId;
18use brokk_bifrost_core::analyzer::rust_facts::RustItemSourceFacts;
19use brokk_bifrost_core::analyzer::source_facts::{
20 PrimarySourceFactCollector, SourceDeclarationId, SourceImportId, SourceOccurrenceId,
21};
22use brokk_bifrost_core::analyzer::structural::callable::CallSiteContext;
23use brokk_bifrost_core::analyzer::structural::collector::StructuralFactCollector;
24use brokk_bifrost_core::analyzer::structural::resolution::DeclaredVisibility;
25use brokk_bifrost_core::analyzer::structural::spec::{CompiledKinds, StructuralSpec};
26use brokk_bifrost_core::analyzer::tree_walk::{ParentIndex, WalkControl, walk_named_tree_preorder};
27use brokk_bifrost_core::analyzer::{CodeUnit, ProjectFile};
28use brokk_bifrost_core::hash::{HashMap, HashSet};
29use brokk_bifrost_core::path_normalization::NormalizePath;
30use brokk_bifrost_core::text_utils::{compute_line_starts, find_line_index_for_offset};
31use std::borrow::Cow;
32use std::path::{Path, PathBuf};
33use tree_sitter::{Node, Parser, Tree};
34
35use crate::source_properties::{python_annotation_references, python_return_type_identity};
36use crate::structural::{PYTHON_STRUCTURAL_SPEC, python_parenthesized_expression_child};
37
38pub fn python_module_fq(project: &dyn Project, file: &ProjectFile) -> FqName {
51 FqName::from_package_components(&python_module_components(project, file))
52}
53
54fn python_module_components(project: &dyn Project, file: &ProjectFile) -> Vec<String> {
55 python_module_components_from_root(file, &python_import_root(project, file))
56}
57
58pub fn python_module_components_from_name(file: &ProjectFile, name: &str) -> Option<Vec<String>> {
62 let roots = file.rel_path().parent()?.ancestors().collect::<Vec<_>>();
67 roots.into_iter().rev().find_map(|root| {
68 let components = python_module_components_from_root(file, root);
69 (components.join(".") == name).then_some(components)
70 })
71}
72
73pub fn python_project_module_components(file: &ProjectFile) -> Vec<String> {
86 python_module_components_from_root(file, Path::new(""))
87}
88
89fn python_module_components_from_root(file: &ProjectFile, import_root_rel: &Path) -> Vec<String> {
92 let mut components = python_package_components_for_file(file, import_root_rel);
93 let module_name = file
94 .rel_path()
95 .file_stem()
96 .and_then(|stem| stem.to_str())
97 .unwrap_or_default();
98 if module_name != "__init__" || components.is_empty() {
99 components.push(module_name.to_string());
100 }
101 components
102}
103
104pub trait PythonImportRootInputs {
106 fn contains(&self, relative_path: &Path) -> bool;
107 fn configuration_source(&self, relative_path: &Path) -> std::io::Result<Option<Cow<'_, str>>>;
108}
109
110struct ProjectImportRootInputs<'a>(&'a dyn Project);
111impl PythonImportRootInputs for ProjectImportRootInputs<'_> {
112 fn contains(&self, relative_path: &Path) -> bool {
113 self.0.file_by_rel_path(relative_path).is_some()
114 }
115 fn configuration_source(&self, relative_path: &Path) -> std::io::Result<Option<Cow<'_, str>>> {
116 Ok(self
117 .0
118 .file_by_rel_path(relative_path)
119 .and_then(|file| self.0.read_source(&file).ok())
120 .map(Cow::Owned))
121 }
122}
123
124pub fn python_import_root(project: &dyn Project, file: &ProjectFile) -> PathBuf {
136 python_import_root_with_inputs(
137 file.root(),
138 file.rel_path(),
139 &ProjectImportRootInputs(project),
140 )
141 .expect("raw project import-root inputs are infallible")
142}
143
144pub fn python_import_root_with_inputs(
146 root: &Path,
147 relative_path: &Path,
148 inputs: &impl PythonImportRootInputs,
149) -> std::io::Result<PathBuf> {
150 let Some(parent_rel) = relative_path.parent() else {
151 return Ok(PathBuf::new());
152 };
153 if parent_rel.as_os_str().is_empty() {
154 return Ok(PathBuf::new());
155 }
156 if let Some(import_root_rel) = python_configured_import_root(root, parent_rel, inputs)? {
157 return Ok(import_root_rel);
158 }
159 let mut effective_package_root_rel: Option<&Path> = None;
160 for path in parent_rel.ancestors() {
161 if inputs.contains(&path.join("__init__.py")) {
162 effective_package_root_rel = Some(path);
163 }
164 }
165 Ok(effective_package_root_rel
166 .and_then(Path::parent)
167 .unwrap_or_else(|| Path::new(""))
168 .to_path_buf())
169}
170
171fn python_package_components_for_file(file: &ProjectFile, import_root_rel: &Path) -> Vec<String> {
172 let Some(parent_rel) = file.rel_path().parent() else {
173 return Vec::new();
174 };
175 let relative_package = parent_rel
176 .strip_prefix(import_root_rel)
177 .expect("a Python import root is a prefix of the paths it names modules for");
178 path_components(relative_package)
179}
180
181pub const PYTHON_CONFIGURATION_INPUT_NAMES: [&str; 3] =
183 ["pyproject.toml", "setup.py", "__init__.py"];
184
185fn python_configured_import_root(
191 workspace_root: &Path,
192 parent_rel: &Path,
193 inputs: &impl PythonImportRootInputs,
194) -> std::io::Result<Option<PathBuf>> {
195 let mut manifest_dir_rel = Some(parent_rel);
196 while let Some(directory) = manifest_dir_rel {
197 let manifest_dir = workspace_root.join(directory);
198 let mut roots = inputs
199 .configuration_source(&directory.join("pyproject.toml"))?
200 .map(|source| parse_setuptools_where_entries(&source))
201 .unwrap_or_default()
202 .iter()
203 .map(|entry| manifest_dir.join(entry).normalize())
204 .filter_map(|root| {
205 root.strip_prefix(workspace_root)
206 .ok()
207 .map(Path::to_path_buf)
208 })
209 .filter(|root| parent_rel.starts_with(root))
210 .collect::<Vec<_>>();
211 roots.sort_by_key(|root| root.components().count());
212 if let Some(root) = roots.pop() {
213 return Ok(Some(root));
214 }
215 if let Some(package_dir) = inputs
216 .configuration_source(&directory.join("setup.py"))?
217 .and_then(|source| parse_setuptools_setup_py_import_root(&source))
218 {
219 let root = manifest_dir.join(package_dir).normalize();
220 if let Ok(root) = root.strip_prefix(workspace_root)
221 && parent_rel.starts_with(root)
222 {
223 return Ok(Some(root.to_path_buf()));
224 }
225 }
226 manifest_dir_rel = directory.parent();
227 }
228 Ok(None)
229}
230
231pub fn python_declared_import_root(project: &dyn Project, file: &ProjectFile) -> Option<PathBuf> {
243 let parent_rel = file.rel_path().parent()?;
244 if parent_rel.as_os_str().is_empty() {
245 return None;
246 }
247 python_configured_import_root(file.root(), parent_rel, &ProjectImportRootInputs(project))
248 .expect("raw project import-root inputs are infallible")
249}
250
251pub(crate) fn python_repeated_root_extension_dir(
276 file: &ProjectFile,
277 source_root_rel: &Path,
278 declared_root_rel: &Path,
279) -> Option<PathBuf> {
280 let implied_rel = source_root_rel.strip_prefix(declared_root_rel).ok()?;
281 let implied = path_components(implied_rel);
282 if implied.is_empty()
283 || !python_module_components_from_root(file, source_root_rel).starts_with(&implied)
284 {
285 return None;
286 }
287 Some(source_root_rel.join(implied_rel))
288}
289
290fn parse_setuptools_setup_py_import_root(source: &str) -> Option<PathBuf> {
296 let tree = parse_python_tree(source)?;
297 let root = tree.root_node();
298 if root.has_error() {
299 return None;
300 }
301 let mut import_root = None;
302 for (call, setup_bindings) in setup_py_setup_calls(source, root) {
303 let candidate = setup_py_import_root_from_call(call, source, &setup_bindings)?;
304 if import_root
305 .replace(candidate.clone())
306 .is_some_and(|root| root != candidate)
307 {
308 return None;
309 }
310 }
311 import_root
312}
313
314pub fn setuptools_setup_py_python_requires(setup_py: &Path) -> Option<String> {
322 let source = std::fs::read_to_string(setup_py).ok()?;
323 setuptools_setup_py_python_requires_from_source(&source)
324}
325
326pub fn setuptools_setup_py_python_requires_from_source(source: &str) -> Option<String> {
328 let tree = parse_python_tree(source)?;
329 let root = tree.root_node();
330 if root.has_error() {
331 return None;
332 }
333 let mut requirement: Option<String> = None;
334 for (call, _) in setup_py_setup_calls(source, root) {
335 let Some(arguments) = call.child_by_field_name("arguments") else {
336 continue;
337 };
338 if arguments.kind() != "argument_list" {
339 continue;
340 }
341 let mut cursor = arguments.walk();
342 for argument in arguments.named_children(&mut cursor) {
343 if argument.kind() != "keyword_argument" {
344 continue;
345 }
346 let Some(name) = argument.child_by_field_name("name") else {
347 continue;
348 };
349 if py_node_text(name, source).trim() != "python_requires" {
350 continue;
351 }
352 let value = argument.child_by_field_name("value")?;
353 let declared = python_plain_string_literal(value, source)?.to_owned();
354 if requirement
355 .replace(declared.clone())
356 .is_some_and(|previous| previous != declared)
357 {
358 return None;
359 }
360 }
361 }
362 requirement
363}
364
365#[derive(Debug, Clone)]
368pub struct SetuptoolsLiteralArgument {
369 pub name: String,
370 pub value: Option<SetuptoolsLiteralValue>,
371 pub byte_range: std::ops::Range<usize>,
372 pub call_index: usize,
373}
374
375#[derive(Debug, Clone)]
376pub enum SetuptoolsLiteralValue {
377 String(String),
378 Strings(Vec<String>),
379 Extras(Vec<(String, Vec<String>)>),
380}
381
382pub fn setuptools_setup_py_literal_arguments(
383 source: &str,
384) -> Option<Vec<SetuptoolsLiteralArgument>> {
385 let tree = parse_python_tree(source)?;
386 let root = tree.root_node();
387 if root.has_error() {
388 return None;
389 }
390 let mut output = Vec::new();
391 for (call_index, (call, _)) in setup_py_setup_calls(source, root).into_iter().enumerate() {
392 let arguments = call.child_by_field_name("arguments")?;
393 let mut cursor = arguments.walk();
394 for argument in arguments.named_children(&mut cursor) {
395 if argument.kind() == "dictionary_splat" {
396 output.push(SetuptoolsLiteralArgument {
397 name: "**kwargs".to_owned(),
398 value: None,
399 byte_range: argument.byte_range(),
400 call_index,
401 });
402 continue;
403 }
404 if argument.kind() != "keyword_argument" {
405 continue;
406 }
407 let name = py_node_text(argument.child_by_field_name("name")?, source).to_owned();
408 let node = argument.child_by_field_name("value")?;
409 let value = match name.as_str() {
410 "name" | "version" | "python_requires" => python_plain_string_literal(node, source)
411 .map(|value| SetuptoolsLiteralValue::String(value.to_owned())),
412 "install_requires" | "setup_requires" | "tests_require" => {
413 setuptools_literal_strings(node, source).map(SetuptoolsLiteralValue::Strings)
414 }
415 "extras_require" => {
416 let mut entries = Vec::new();
417 let mut cursor = node.walk();
418 let mut complete = node.kind() == "dictionary";
419 for pair in node.named_children(&mut cursor) {
420 let entry = pair
421 .child_by_field_name("key")
422 .and_then(|key| python_plain_string_literal(key, source))
423 .zip(
424 pair.child_by_field_name("value")
425 .and_then(|value| setuptools_literal_strings(value, source)),
426 );
427 if let Some((key, values)) = entry {
428 entries.push((key.to_owned(), values));
429 } else {
430 complete = false;
431 }
432 }
433 complete.then_some(SetuptoolsLiteralValue::Extras(entries))
434 }
435 _ => continue,
436 };
437 output.push(SetuptoolsLiteralArgument {
438 name,
439 value,
440 byte_range: node.byte_range(),
441 call_index,
442 });
443 }
444 }
445 Some(output)
446}
447
448fn setuptools_literal_strings(node: Node<'_>, source: &str) -> Option<Vec<String>> {
449 if !matches!(node.kind(), "list" | "tuple") {
450 return None;
451 }
452 let mut cursor = node.walk();
453 node.named_children(&mut cursor)
454 .filter(|child| child.kind() != "comment")
455 .map(|child| python_plain_string_literal(child, source).map(str::to_owned))
456 .collect()
457}
458
459fn setup_py_setup_calls<'tree>(
467 source: &str,
468 root: Node<'tree>,
469) -> Vec<(Node<'tree>, HashMap<Vec<String>, String>)> {
470 let mut setup_bindings: HashMap<Vec<String>, String> = HashMap::default();
471 let mut calls = Vec::new();
472 let mut cursor = root.walk();
473
474 for statement in root.named_children(&mut cursor) {
475 if matches!(
476 statement.kind(),
477 "import_statement" | "import_from_statement"
478 ) {
479 for binding in setup_py_bound_names(statement, source) {
480 setup_bindings.retain(|path, _| path.first() != Some(&binding));
481 }
482 for import in python_import_infos_from_node(statement, source) {
483 if import.is_wildcard {
484 setup_bindings.clear();
485 continue;
486 }
487 let Some(path) = import.path else { continue };
488 let segments = path.segments.iter().map(String::as_str).collect::<Vec<_>>();
489 match path.kind {
490 Some(StructuredImportPathKind::ImportFrom) => {
491 let function_name = match segments.as_slice() {
492 ["setuptools", "setup"] | ["distutils", "core", "setup"] => "setup",
493 ["setuptools", "find_packages"] => "find_packages",
494 ["setuptools", "find_namespace_packages"] => "find_namespace_packages",
495 _ => continue,
496 };
497 setup_bindings.insert(
498 vec![import.identifier.expect("imported function binds a name")],
499 function_name.to_string(),
500 );
501 }
502 Some(StructuredImportPathKind::Namespace) => {
503 let function_names = match segments.as_slice() {
504 ["setuptools"] => {
505 ["setup", "find_packages", "find_namespace_packages"].as_slice()
506 }
507 ["distutils", "core"] => ["setup"].as_slice(),
508 _ => continue,
509 };
510 let binding_prefix = import
511 .alias
512 .map(|alias| vec![alias])
513 .unwrap_or_else(|| path.segments.clone());
514 for function_name in function_names {
515 let mut callable = binding_prefix.clone();
516 callable.push((*function_name).to_string());
517 setup_bindings.insert(callable, (*function_name).to_string());
518 }
519 }
520 _ => continue,
521 }
522 }
523 continue;
524 }
525
526 if statement.kind() == "expression_statement"
527 && statement.named_child_count() == 1
528 && let Some(call) = statement.named_child(0)
529 && call.kind() == "call"
530 && setup_py_call_imported_function(call, source, &setup_bindings)
531 .is_some_and(|function| function == "setup")
532 {
533 calls.push((call, setup_bindings.clone()));
534 }
535
536 for binding in setup_py_bound_names(statement, source) {
537 setup_bindings.retain(|path, _| path.first() != Some(&binding));
538 }
539 }
540 calls
541}
542
543fn setup_py_bound_names(statement: Node<'_>, source: &str) -> Vec<String> {
550 let mut names = Vec::new();
551 let mut pending = vec![statement];
552 while let Some(node) = pending.pop() {
553 for binding in python_direct_scope_bindings_bounded(node, source, || true)
554 .expect("unbounded setup.py binding walk")
555 {
556 let name = py_node_text(binding.declaration, source).trim();
557 if !name.is_empty() {
558 names.push(name.to_string());
559 }
560 }
561 let excluded_body = matches!(
562 node.kind(),
563 "function_definition" | "class_definition" | "lambda"
564 )
565 .then(|| node.child_by_field_name("body").map(|body| body.id()))
566 .flatten();
567 let mut cursor = node.walk();
568 pending.extend(
569 node.named_children(&mut cursor)
570 .filter(|child| Some(child.id()) != excluded_body),
571 );
572 }
573 names
574}
575
576fn setup_py_call_imported_function<'a>(
577 call: Node<'_>,
578 source: &str,
579 setup_bindings: &'a HashMap<Vec<String>, String>,
580) -> Option<&'a str> {
581 let mut function = call.child_by_field_name("function")?;
582 let mut path = Vec::new();
583 while function.kind() == "attribute" {
584 let attribute = function.child_by_field_name("attribute")?;
585 path.push(py_node_text(attribute, source).to_string());
586 let object = function.child_by_field_name("object")?;
587 function = object;
588 }
589 if function.kind() != "identifier" {
590 return None;
591 }
592 path.push(py_node_text(function, source).to_string());
593 path.reverse();
594 setup_bindings.get(&path).map(String::as_str)
595}
596
597fn setup_py_import_root_from_call(
598 call: Node<'_>,
599 source: &str,
600 setup_bindings: &HashMap<Vec<String>, String>,
601) -> Option<PathBuf> {
602 let arguments = call.child_by_field_name("arguments")?;
603 if arguments.kind() != "argument_list" {
604 return None;
605 }
606
607 let mut packages = None;
608 let mut package_dir = None;
609 let mut cursor = arguments.walk();
610 for argument in arguments.named_children(&mut cursor) {
611 if argument.kind() == "comment" {
612 continue;
613 }
614 if argument.kind() != "keyword_argument" {
616 return None;
617 }
618 let name = argument.child_by_field_name("name")?;
619 let value = argument.child_by_field_name("value")?;
620 match py_node_text(name, source).trim() {
621 "packages" if packages.is_none() => packages = Some(value),
622 "packages" => return None,
623 "package_dir" if package_dir.is_none() => package_dir = Some(value),
624 "package_dir" => return None,
625 _ => {}
626 }
627 }
628
629 let packages = packages?;
630 if let Some(package_dir) = package_dir {
631 return setup_py_static_package_dir(package_dir, source);
632 }
633 if setup_py_nonempty_literal_packages(packages, source) {
634 return Some(PathBuf::new());
635 }
636 setup_py_discovery_root_from_call(packages, source, setup_bindings)
637}
638
639fn setup_py_discovery_root_from_call(
640 call: Node<'_>,
641 source: &str,
642 setup_bindings: &HashMap<Vec<String>, String>,
643) -> Option<PathBuf> {
644 let function = setup_py_call_imported_function(call, source, setup_bindings)?;
645 if !matches!(function, "find_packages" | "find_namespace_packages") {
646 return None;
647 }
648 let arguments = call.child_by_field_name("arguments")?;
649 if arguments.kind() != "argument_list" {
650 return None;
651 }
652
653 let mut where_value = None;
654 let mut seen_exclude = false;
655 let mut seen_include = false;
656 let mut positional_index = 0;
657 let mut cursor = arguments.walk();
658 for argument in arguments.named_children(&mut cursor) {
659 if argument.kind() == "comment" {
660 continue;
661 }
662 if matches!(argument.kind(), "list_splat" | "dictionary_splat") {
663 return None;
664 }
665 if argument.kind() == "keyword_argument" {
666 let name = py_node_text(argument.child_by_field_name("name")?, source).trim();
667 let value = argument.child_by_field_name("value")?;
668 match name {
669 "where" if where_value.is_none() => where_value = Some(value),
670 "where" => return None,
671 "exclude" if !seen_exclude => seen_exclude = true,
672 "exclude" => return None,
673 "include" if !seen_include => seen_include = true,
674 "include" => return None,
675 _ => return None,
676 }
677 continue;
678 }
679
680 let slot = positional_index;
681 positional_index += 1;
682 match slot {
683 0 if where_value.is_none() => where_value = Some(argument),
684 0 => return None,
685 1 if !seen_exclude => seen_exclude = true,
686 1 => return None,
687 2 if !seen_include => seen_include = true,
688 2 => return None,
689 _ => return None,
690 }
691 }
692
693 where_value
694 .map(|value| python_plain_string_literal(value, source))
695 .unwrap_or(Some(""))
696 .map(PathBuf::from)
697}
698
699fn setup_py_nonempty_literal_packages(value: Node<'_>, source: &str) -> bool {
700 let value = setup_py_unwrap_parenthesized(value);
701 if !matches!(value.kind(), "list" | "set" | "tuple") {
702 return false;
703 }
704 let mut cursor = value.walk();
705 let mut nonempty = false;
706 for element in value.named_children(&mut cursor) {
707 if element.kind() == "comment" {
708 continue;
709 }
710 let Some(package) = python_plain_string_literal(element, source) else {
711 return false;
712 };
713 if package.is_empty() {
714 return false;
715 }
716 nonempty = true;
717 }
718 nonempty
719}
720
721fn setup_py_static_package_dir(value: Node<'_>, source: &str) -> Option<PathBuf> {
722 let value = setup_py_unwrap_parenthesized(value);
723 if value.kind() != "dictionary" {
724 return None;
725 }
726 let mut cursor = value.walk();
727 let mut pairs = value
728 .named_children(&mut cursor)
729 .filter(|node| node.kind() != "comment");
730 let Some(pair) = pairs.next() else {
731 return Some(PathBuf::new());
732 };
733 if pairs.next().is_some() || pair.kind() != "pair" {
734 return None;
735 }
736 let key = python_plain_string_literal(pair.child_by_field_name("key")?, source)?;
737 if !key.is_empty() {
738 return None;
739 }
740 let root = python_plain_string_literal(pair.child_by_field_name("value")?, source)?;
741 let root = PathBuf::from(root);
742 (!root.is_absolute()).then_some(root)
743}
744
745fn setup_py_unwrap_parenthesized(node: Node<'_>) -> Node<'_> {
746 brokk_bifrost_core::analyzer::tree_walk::unwrap_parenthesized_expression_with(
747 node,
748 python_parenthesized_expression_child,
749 )
750}
751
752fn parse_setuptools_where_entries(source: &str) -> Vec<String> {
753 let Ok(document) = source.parse::<toml::Value>() else {
754 return Vec::new();
755 };
756 document
757 .get("tool")
758 .and_then(|tool| tool.get("setuptools"))
759 .and_then(|setuptools| setuptools.get("packages"))
760 .and_then(|packages| packages.get("find"))
761 .and_then(|find| find.get("where"))
762 .and_then(toml::Value::as_array)
763 .map(|entries| {
764 entries
765 .iter()
766 .filter_map(toml::Value::as_str)
767 .map(str::to_string)
768 .collect()
769 })
770 .unwrap_or_default()
771}
772
773fn path_components(path: &Path) -> Vec<String> {
774 path.components()
775 .map(|component| component.as_os_str().to_string_lossy().to_string())
776 .filter(|component| !component.is_empty())
777 .collect()
778}
779
780pub fn python_is_decorated_function_boundary(node: Node<'_>) -> bool {
781 if node.kind() != "decorated_definition" {
782 return false;
783 }
784 let mut cursor = node.walk();
785 node.named_children(&mut cursor)
786 .any(|child| child.kind() == "function_definition")
787}
788
789#[derive(Clone)]
790pub struct Scope {
791 kind: ScopeKind,
792 path: String,
793 fq: FqName,
799 code_unit: Option<CodeUnit>,
800 method_receiver: Option<String>,
801}
802
803#[derive(Clone, Copy, PartialEq, Eq)]
804enum ScopeKind {
805 Class,
806 Function,
807}
808
809pub struct PythonVisitor<'source, 'parsed, 'structural>
810where
811 'source: 'structural,
812{
813 pub file: &'parsed ProjectFile,
814 pub source: &'source str,
815 pub package_name: &'parsed str,
816 module_fq: &'parsed FqName,
817 pub parsed: &'parsed mut ParsedFile,
818 pub module: Option<CodeUnit>,
819 overload_decorators: PythonOverloadDecoratorBindings,
820 source_facts: PrimarySourceFactCollector<'source>,
821 structural: StructuralFactCollector<'structural, 'structural>,
822 structural_kinds: &'structural CompiledKinds,
823 call_site_context: &'structural CallSiteContext,
824 structural_parents: Vec<Option<u32>>,
825 source_imports: Vec<SourceImportFact>,
826 generic_imports: Vec<SourceImportId>,
827 source_declaration_units: Vec<(SourceDeclarationId, CodeUnit)>,
828 source_declaration_unit_keys: HashSet<CodeUnit>,
831 python_source_facts: PythonSourceFacts,
832 python_callable_return_by_node: HashMap<usize, usize>,
833 overload_metadata: Vec<(CodeUnit, usize, Vec<PythonOverloadDecoratorName>)>,
834}
835
836struct PythonFrame<'tree> {
837 node: Node<'tree>,
838 scope: Vec<Scope>,
839 module_control_depth: usize,
840 declaration: bool,
841 wrapper: Option<Node<'tree>>,
842 lexical_scopes: Vec<Node<'tree>>,
843}
844
845enum PythonWork<'tree> {
846 Enter(PythonFrame<'tree>),
847 Exit,
848}
849
850struct DeclarationChildren<'tree> {
851 body: Option<(Node<'tree>, Vec<Scope>)>,
852 container_scope: Option<Vec<Scope>>,
853 container_depth: usize,
854}
855
856impl<'source, 'parsed, 'structural> PythonVisitor<'source, 'parsed, 'structural>
857where
858 'source: 'structural,
859{
860 pub fn visit_container(
861 &mut self,
862 node: Node<'_>,
863 scope: &[Scope],
864 module_control_depth: usize,
865 ) {
866 let mut stack = vec![PythonWork::Enter(PythonFrame {
867 node,
868 scope: scope.to_vec(),
869 module_control_depth,
870 declaration: true,
871 wrapper: None,
872 lexical_scopes: Vec::new(),
873 })];
874 while let Some(work) = stack.pop() {
875 let PythonWork::Enter(frame) = work else {
876 self.structural_parents
877 .pop()
878 .expect("Python structural parent stack must balance");
879 continue;
880 };
881 let structural_parent = self.structural_parents.last().copied().flatten();
882 let structural_id = self.admit_structural(frame.node, structural_parent);
883 self.structural_parents
884 .push(structural_id.or(structural_parent));
885
886 if frame.node.kind() == "identifier" {
887 let text = py_node_text(frame.node, self.source).trim();
888 if !text.is_empty() {
889 self.parsed.type_identifiers.insert(text.to_string());
890 }
891 }
892
893 if frame.node.kind() == "function_definition" {
894 self.capture_function_return_fact(frame.node, frame.wrapper);
895 }
896
897 if matches!(
898 frame.node.kind(),
899 "import_statement" | "import_from_statement"
900 ) {
901 self.visit_import_statement(frame.node, &frame.lexical_scopes);
902 }
903
904 let declaration_children = if frame.declaration {
905 self.visit_statement(
906 frame.node,
907 &frame.scope,
908 frame.module_control_depth,
909 frame.wrapper,
910 )
911 } else {
912 DeclarationChildren {
913 body: None,
914 container_scope: None,
915 container_depth: frame.module_control_depth,
916 }
917 };
918
919 let mut children = Vec::new();
920 let mut cursor = frame.node.walk();
921 for child in frame.node.named_children(&mut cursor) {
922 let mut child_scope = frame.scope.clone();
923 let mut declaration = false;
924 let mut wrapper = None;
925 let mut child_depth = frame.module_control_depth;
926
927 if let Some((body, scope)) = &declaration_children.body
928 && child.id() == body.id()
929 {
930 declaration = true;
931 child_scope = scope.clone();
932 } else if let Some(scope) = &declaration_children.container_scope {
933 declaration = true;
934 child_scope = scope.clone();
935 child_depth = declaration_children.container_depth;
936 } else if frame.declaration
937 && frame.node.kind() == "decorated_definition"
938 && matches!(child.kind(), "class_definition" | "function_definition")
939 {
940 declaration = true;
943 wrapper = Some(frame.node);
944 }
945
946 let mut lexical_scopes = frame.lexical_scopes.clone();
947 if matches!(
948 frame.node.kind(),
949 "class_definition" | "function_definition" | "lambda"
950 ) {
951 lexical_scopes.push(frame.node);
952 }
953 children.push(PythonFrame {
954 node: child,
955 scope: child_scope,
956 module_control_depth: child_depth,
957 declaration,
958 wrapper,
959 lexical_scopes,
960 });
961 }
962
963 stack.push(PythonWork::Exit);
964 stack.extend(children.into_iter().rev().map(PythonWork::Enter));
965 }
966 }
967
968 fn admit_structural(&mut self, node: Node<'_>, parent: Option<u32>) -> Option<u32> {
969 if !node.is_named() {
970 return parent;
971 }
972 let Some(raw_kind) = self.structural_kinds.kind_of(&node) else {
973 return parent;
974 };
975 if !PYTHON_STRUCTURAL_SPEC.should_extract(node, raw_kind) {
976 return parent;
977 }
978 let kind = PYTHON_STRUCTURAL_SPEC.refine_kind(
979 node,
980 raw_kind,
981 parent.map(|id| self.structural.normalized_kind(id)),
982 self.source,
983 self.call_site_context,
984 );
985 let fact_id = self
986 .structural
987 .enter(node, kind, parent, &mut self.source_facts)
988 .expect("Python structural fact collection must remain unbounded");
989 let mut sink = self.structural.role_sink(&mut self.source_facts);
990 PYTHON_STRUCTURAL_SPEC.extract(node, kind, &mut sink);
991 self.structural
992 .accept_roles(fact_id, sink.into_parts())
993 .expect("Python structural role collection must remain unbounded");
994 Some(fact_id)
995 }
996
997 fn replace_source_declaration(
998 &mut self,
999 declaration_node: Node<'_>,
1000 name_node: Option<Node<'_>>,
1001 unit: CodeUnit,
1002 metadata_ordinal: Option<usize>,
1003 ) -> SourceDeclarationId {
1004 self.source_declaration_units
1005 .retain(|(_, existing)| existing != &unit);
1006 self.parsed
1007 .source_declaration_metadata
1008 .retain(|link| link.unit != unit);
1009 self.record_source_declaration(declaration_node, name_node, unit, metadata_ordinal)
1010 }
1011
1012 fn record_source_declaration(
1013 &mut self,
1014 declaration_node: Node<'_>,
1015 name_node: Option<Node<'_>>,
1016 unit: CodeUnit,
1017 metadata_ordinal: Option<usize>,
1018 ) -> SourceDeclarationId {
1019 let declaration = self.source_facts.declare_node(declaration_node, name_node);
1020 self.link_source_declaration(declaration, unit, metadata_ordinal);
1021 declaration
1022 }
1023
1024 fn capture_function_return_fact<'tree>(
1025 &mut self,
1026 node: Node<'tree>,
1027 wrapper: Option<Node<'tree>>,
1028 ) {
1029 let declaration_node = wrapper
1030 .or_else(|| {
1031 node.parent()
1032 .filter(|parent| parent.kind() == "decorated_definition")
1033 })
1034 .unwrap_or(node);
1035 let declaration = self
1036 .source_facts
1037 .declare_node(declaration_node, node.child_by_field_name("name"));
1038 let return_annotation = node
1039 .child_by_field_name("return_type")
1040 .map(|annotation| self.source_facts.intern_node(annotation));
1041 let annotation_references = node
1042 .child_by_field_name("return_type")
1043 .map(|annotation| {
1044 python_annotation_references(annotation, self.source, &mut self.source_facts)
1045 })
1046 .unwrap_or_default();
1047 let runtime_type = node
1048 .child_by_field_name("return_type")
1049 .and_then(|annotation| python_return_type_identity(annotation, self.source));
1050 let fact_index = self.python_source_facts.callable_returns.len();
1051 self.python_source_facts
1052 .callable_returns
1053 .push(PythonCallableReturnFact {
1054 declaration,
1055 return_annotation,
1056 annotation_references,
1057 runtime_type,
1058 });
1059 assert!(
1060 self.python_callable_return_by_node
1061 .insert(node.id(), fact_index)
1062 .is_none(),
1063 "each Python function definition must have one callable return fact"
1064 );
1065 }
1066
1067 fn link_source_declaration(
1068 &mut self,
1069 declaration: SourceDeclarationId,
1070 unit: CodeUnit,
1071 metadata_ordinal: Option<usize>,
1072 ) {
1073 self.source_declaration_unit_keys.insert(unit.clone());
1074 self.source_declaration_units
1075 .push((declaration, unit.clone()));
1076 if let Some(metadata_ordinal) = metadata_ordinal {
1077 self.parsed
1078 .source_declaration_metadata
1079 .push(SourceDeclarationMetadataLink {
1080 declaration,
1081 unit,
1082 metadata_ordinal,
1083 });
1084 }
1085 }
1086
1087 fn visit_statement<'tree>(
1088 &mut self,
1089 node: Node<'tree>,
1090 scope: &[Scope],
1091 module_control_depth: usize,
1092 wrapper: Option<Node<'tree>>,
1093 ) -> DeclarationChildren<'tree> {
1094 match node.kind() {
1095 "decorated_definition" => DeclarationChildren {
1096 body: None,
1097 container_scope: None,
1098 container_depth: module_control_depth,
1099 },
1100 "class_definition" | "function_definition" => {
1101 self.visit_definition(node, wrapper, scope, module_control_depth)
1102 }
1103 "expression_statement" => {
1104 self.visit_expression_statement(node, scope, module_control_depth);
1105 DeclarationChildren {
1106 body: None,
1107 container_scope: None,
1108 container_depth: module_control_depth,
1109 }
1110 }
1111 "import_statement" | "import_from_statement" => DeclarationChildren {
1112 body: None,
1113 container_scope: None,
1114 container_depth: module_control_depth,
1115 },
1116 "if_statement" | "try_statement" | "with_statement" | "for_statement"
1117 | "while_statement" => {
1118 let next_depth = if scope.is_empty() {
1119 module_control_depth + 1
1120 } else {
1121 module_control_depth
1122 };
1123 DeclarationChildren {
1124 body: None,
1125 container_scope: Some(scope.to_vec()),
1126 container_depth: next_depth,
1127 }
1128 }
1129 "elif_clause" | "else_clause" | "except_clause" | "finally_clause" => {
1130 DeclarationChildren {
1131 body: None,
1132 container_scope: Some(scope.to_vec()),
1133 container_depth: module_control_depth,
1134 }
1135 }
1136 "block" | "module" => DeclarationChildren {
1137 body: None,
1138 container_scope: Some(scope.to_vec()),
1139 container_depth: module_control_depth,
1140 },
1141 _ => DeclarationChildren {
1142 body: None,
1143 container_scope: None,
1144 container_depth: module_control_depth,
1145 },
1146 }
1147 }
1148
1149 fn visit_definition<'tree>(
1150 &mut self,
1151 definition: Node<'tree>,
1152 wrapper: Option<Node<'tree>>,
1153 scope: &[Scope],
1154 module_control_depth: usize,
1155 ) -> DeclarationChildren<'tree> {
1156 match definition.kind() {
1157 "class_definition" => self.visit_class_definition(
1158 definition,
1159 wrapper.unwrap_or(definition),
1160 scope,
1161 module_control_depth,
1162 ),
1163 "function_definition" => self.visit_function_definition(
1164 definition,
1165 wrapper.unwrap_or(definition),
1166 scope,
1167 module_control_depth,
1168 ),
1169 _ => DeclarationChildren {
1170 body: None,
1171 container_scope: None,
1172 container_depth: module_control_depth,
1173 },
1174 }
1175 }
1176
1177 fn visit_class_definition<'tree>(
1178 &mut self,
1179 node: Node<'tree>,
1180 range_node: Node<'tree>,
1181 scope: &[Scope],
1182 module_control_depth: usize,
1183 ) -> DeclarationChildren<'tree> {
1184 let Some(name_node) = node.child_by_field_name("name") else {
1185 return DeclarationChildren {
1186 body: None,
1187 container_scope: None,
1188 container_depth: module_control_depth,
1189 };
1190 };
1191 let name = py_node_text(name_node, self.source).trim();
1192 if name.is_empty() {
1193 return DeclarationChildren {
1194 body: None,
1195 container_scope: None,
1196 container_depth: module_control_depth,
1197 };
1198 }
1199
1200 let capture = !scope.is_empty() || module_control_depth <= 1;
1201
1202 let short_name = scope
1203 .last()
1204 .map(|parent| format!("{}${name}", parent.path))
1205 .unwrap_or_else(|| name.to_string());
1206 let fq = match scope.last() {
1212 Some(parent) => parent
1213 .fq
1214 .clone()
1215 .with_pushed(segment_interner().intern(name, SegmentKind::Nested)),
1216 None => self
1217 .module_fq
1218 .clone()
1219 .with_pushed(segment_interner().intern(name, SegmentKind::Type)),
1220 };
1221 let code_unit = CodeUnit::new_fq(
1222 self.file.clone(),
1223 CodeUnitType::Class,
1224 self.package_name.to_string(),
1225 short_name.clone(),
1226 fq.clone(),
1227 );
1228 if capture {
1229 self.parsed
1230 .replace_code_unit(code_unit.clone(), range_node, self.source, None, None);
1231 self.parsed.add_signature(
1232 code_unit.clone(),
1233 python_class_signature(range_node, self.source),
1234 );
1235 if let Some(module) = &self.module
1236 && scope.is_empty()
1237 {
1238 self.parsed.add_child(module.clone(), code_unit.clone());
1239 }
1240 if let Some(parent) = scope.last()
1241 && let Some(parent_cu) = &parent.code_unit
1242 {
1243 self.parsed.add_child(parent_cu.clone(), code_unit.clone());
1244 }
1245 self.parsed.set_raw_supertypes(
1246 code_unit.clone(),
1247 extract_python_supertypes(node, self.source),
1248 );
1249 self.replace_source_declaration(range_node, Some(name_node), code_unit.clone(), None);
1250 }
1251
1252 let mut next_scope = scope.to_vec();
1253 if capture {
1254 next_scope.push(Scope {
1255 kind: ScopeKind::Class,
1256 path: short_name,
1257 fq,
1258 code_unit: Some(code_unit),
1259 method_receiver: None,
1260 });
1261 }
1262 DeclarationChildren {
1263 body: node
1264 .child_by_field_name("body")
1265 .map(|body| (body, next_scope)),
1266 container_scope: None,
1267 container_depth: module_control_depth,
1268 }
1269 }
1270
1271 fn visit_function_definition<'tree>(
1272 &mut self,
1273 node: Node<'tree>,
1274 range_node: Node<'tree>,
1275 scope: &[Scope],
1276 module_control_depth: usize,
1277 ) -> DeclarationChildren<'tree> {
1278 let Some(name_node) = node.child_by_field_name("name") else {
1279 return DeclarationChildren {
1280 body: None,
1281 container_scope: None,
1282 container_depth: module_control_depth,
1283 };
1284 };
1285 let name = py_node_text(name_node, self.source).trim();
1286 if name.is_empty() {
1287 return DeclarationChildren {
1288 body: None,
1289 container_scope: None,
1290 container_depth: module_control_depth,
1291 };
1292 }
1293
1294 let capture = !python_is_property_mutator(range_node, self.source)
1302 && ((scope.is_empty() && module_control_depth <= 1)
1303 || scope
1304 .last()
1305 .is_some_and(|parent| parent.kind == ScopeKind::Class));
1306 let short_name = if let Some(parent) = scope.last() {
1307 match parent.kind {
1308 ScopeKind::Class => format!("{}.{}", parent.path, name),
1309 ScopeKind::Function => format!("{}${name}", parent.path),
1310 }
1311 } else {
1312 name.to_string()
1313 };
1314 let fq = if let Some(parent) = scope.last() {
1319 match parent.kind {
1320 ScopeKind::Class => parent
1321 .fq
1322 .clone()
1323 .with_pushed(segment_interner().intern(name, SegmentKind::Member)),
1324 ScopeKind::Function => parent
1325 .fq
1326 .clone()
1327 .with_pushed(segment_interner().intern(name, SegmentKind::Nested)),
1328 }
1329 } else {
1330 self.module_fq
1331 .clone()
1332 .with_pushed(segment_interner().intern(name, SegmentKind::Member))
1333 };
1334
1335 if capture {
1336 let code_unit_type = if python_function_has_property_decorator(node, self.source) {
1337 CodeUnitType::Field
1338 } else {
1339 CodeUnitType::Function
1340 };
1341 let signature = node
1342 .child_by_field_name("parameters")
1343 .map(|parameters| py_node_text(parameters, self.source).trim().to_string());
1344 let code_unit = CodeUnit::with_signature_and_fq(
1345 self.file.clone(),
1346 code_unit_type,
1347 self.package_name.to_string(),
1348 short_name.clone(),
1349 signature,
1350 false,
1351 fq.clone(),
1352 );
1353 let replaces_declaration = self.parsed.contains_declaration(&code_unit);
1356 let replaces_links = self.source_declaration_unit_keys.contains(&code_unit);
1357 if replaces_declaration {
1358 self.parsed.replace_code_unit(
1359 code_unit.clone(),
1360 range_node,
1361 self.source,
1362 None,
1363 None,
1364 );
1365 } else {
1366 self.parsed
1367 .add_code_unit(code_unit.clone(), range_node, self.source, None, None);
1368 }
1369 let signature = python_function_signature(node, self.source);
1370 let fact_index = *self
1371 .python_callable_return_by_node
1372 .get(&node.id())
1373 .expect("function return fact must precede declaration admission");
1374 let return_fact = &self.python_source_facts.callable_returns[fact_index];
1375 let return_declaration = return_fact.declaration;
1376 let return_type_identity = return_fact.runtime_type.clone();
1377 let return_type_text = return_fact.return_annotation.map(|annotation| {
1378 let range = self.source_facts.occurrence(annotation).range;
1379 self.source[range.start_byte..range.end_byte].to_string()
1380 });
1381 let metadata_ordinal = self.parsed.add_signature_with_metadata(
1382 code_unit.clone(),
1383 python_signature_metadata(
1384 signature,
1385 node,
1386 self.source,
1387 return_type_text.as_deref(),
1388 return_type_identity,
1389 ),
1390 );
1391 if replaces_links {
1392 self.overload_metadata
1393 .retain(|(existing, _, _)| existing != &code_unit);
1394 self.source_declaration_units
1395 .retain(|(_, existing)| existing != &code_unit);
1396 self.parsed
1397 .source_declaration_metadata
1398 .retain(|link| link.unit != code_unit);
1399 }
1400 self.overload_metadata.push((
1401 code_unit.clone(),
1402 metadata_ordinal,
1403 PythonOverloadDecoratorBindings::overload_decorator_names(node, self.source),
1404 ));
1405 self.link_source_declaration(
1406 return_declaration,
1407 code_unit.clone(),
1408 Some(metadata_ordinal),
1409 );
1410 if let Some(module) = &self.module
1411 && scope.is_empty()
1412 {
1413 self.parsed.add_child(module.clone(), code_unit.clone());
1414 }
1415 if let Some(parent) = scope.last()
1416 && parent.kind == ScopeKind::Class
1417 && let Some(parent_cu) = &parent.code_unit
1418 {
1419 self.parsed.add_child(parent_cu.clone(), code_unit.clone());
1420 }
1421 let scope_code_unit = Some(code_unit);
1422 let mut next_scope = scope.to_vec();
1423 next_scope.push(Scope {
1424 kind: ScopeKind::Function,
1425 path: short_name,
1426 fq,
1427 code_unit: scope_code_unit,
1428 method_receiver: scope
1429 .last()
1430 .is_some_and(|parent| parent.kind == ScopeKind::Class)
1431 .then(|| python_instance_method_receiver_name(node, self.source))
1432 .flatten(),
1433 });
1434 return DeclarationChildren {
1435 body: node
1436 .child_by_field_name("body")
1437 .map(|body| (body, next_scope)),
1438 container_scope: None,
1439 container_depth: module_control_depth,
1440 };
1441 }
1442
1443 let mut next_scope = scope.to_vec();
1444 next_scope.push(Scope {
1445 kind: ScopeKind::Function,
1446 path: short_name,
1447 fq,
1448 code_unit: None,
1449 method_receiver: None,
1450 });
1451 DeclarationChildren {
1452 body: node
1453 .child_by_field_name("body")
1454 .map(|body| (body, next_scope)),
1455 container_scope: None,
1456 container_depth: module_control_depth,
1457 }
1458 }
1459
1460 fn visit_expression_statement(
1461 &mut self,
1462 node: Node<'_>,
1463 scope: &[Scope],
1464 module_control_depth: usize,
1465 ) {
1466 let Some(assignment) = node.named_child(0) else {
1467 return;
1468 };
1469 if assignment.kind() != "assignment" {
1470 return;
1471 }
1472 let targets = python_chained_assignment_targets(assignment);
1473 if targets.is_empty() {
1474 return;
1475 }
1476 for left in &targets {
1477 self.visit_instance_attribute_assignment(*left, scope);
1478 }
1479 let names = targets
1480 .iter()
1481 .flat_map(|left| collect_assigned_name_nodes(*left, self.source));
1482 for name_node in names {
1483 let name = py_node_text(name_node, self.source).trim().to_string();
1484 if name.is_empty() {
1485 continue;
1486 }
1487 let (short_name, fq) = if let Some(parent) = scope.last() {
1488 if parent.kind != ScopeKind::Class {
1489 continue;
1490 }
1491 (
1492 format!("{}.{}", parent.path, name),
1493 parent
1494 .fq
1495 .clone()
1496 .with_pushed(segment_interner().intern(&name, SegmentKind::Member)),
1497 )
1498 } else if module_control_depth <= 1 {
1499 (
1500 name.clone(),
1501 self.module_fq
1502 .clone()
1503 .with_pushed(segment_interner().intern(&name, SegmentKind::Member)),
1504 )
1505 } else {
1506 continue;
1507 };
1508 let code_unit = CodeUnit::new_fq(
1509 self.file.clone(),
1510 CodeUnitType::Field,
1511 self.package_name.to_string(),
1512 short_name,
1513 fq,
1514 );
1515 self.parsed
1521 .add_code_unit(code_unit.clone(), node, self.source, None, None);
1522 self.parsed.add_signature(
1523 code_unit.clone(),
1524 py_node_text(node, self.source).trim().to_string(),
1525 );
1526 self.record_source_declaration(node, Some(name_node), code_unit.clone(), None);
1527 if let Some(module) = &self.module
1528 && scope.is_empty()
1529 {
1530 self.parsed.add_child(module.clone(), code_unit.clone());
1531 }
1532 if let Some(parent) = scope.last()
1533 && parent.kind == ScopeKind::Class
1534 && let Some(parent_cu) = &parent.code_unit
1535 {
1536 self.parsed.add_child(parent_cu.clone(), code_unit);
1537 }
1538 }
1539 }
1540
1541 fn visit_instance_attribute_assignment(&mut self, left: Node<'_>, scope: &[Scope]) {
1542 let Some(function) = scope
1543 .last()
1544 .filter(|scope| scope.kind == ScopeKind::Function)
1545 else {
1546 return;
1547 };
1548 let Some(receiver) = function.method_receiver.as_deref() else {
1549 return;
1550 };
1551 let Some(parent) = scope
1552 .get(scope.len().saturating_sub(2))
1553 .filter(|scope| scope.kind == ScopeKind::Class)
1554 else {
1555 return;
1556 };
1557 let Some(parent_cu) = parent.code_unit.clone() else {
1558 return;
1559 };
1560 for (name, node) in collect_self_assigned_attributes(left, self.source, receiver) {
1561 let code_unit = CodeUnit::new_fq(
1562 self.file.clone(),
1563 CodeUnitType::Field,
1564 self.package_name.to_string(),
1565 format!("{}.{}", parent.path, name),
1566 parent
1567 .fq
1568 .clone()
1569 .with_pushed(segment_interner().intern(&name, SegmentKind::Member)),
1570 );
1571 if !self.parsed.contains_declaration(&code_unit) {
1572 self.parsed.replace_code_unit(
1573 code_unit.clone(),
1574 node,
1575 self.source,
1576 Some(parent_cu.clone()),
1577 Some(parent_cu.clone()),
1578 );
1579 self.record_source_declaration(node, Some(node), code_unit.clone(), None);
1580 }
1581 self.parsed.add_signature(
1582 code_unit.clone(),
1583 py_node_text(node, self.source).trim().to_string(),
1584 );
1585 }
1586 }
1587
1588 fn visit_import_statement(&mut self, node: Node<'_>, lexical_scopes: &[Node<'_>]) {
1589 for syntax in python_import_syntaxes_from_node(node, self.source) {
1590 if lexical_scopes.is_empty() {
1591 self.overload_decorators.collect_import(&syntax.info);
1592 }
1593 let declaration = self.source_facts.intern_node(syntax.declaration);
1594 let target = syntax
1595 .target
1596 .map(|node| self.source_facts.intern_node(node));
1597 let alias_occurrence = syntax.alias.map(|node| self.source_facts.intern_node(node));
1598 let lexical_scopes = lexical_scopes
1599 .iter()
1600 .map(|node| self.source_facts.intern_node(*node))
1601 .collect();
1602 let source_import = SourceImportFact::from_import(
1603 syntax.info,
1604 declaration,
1605 target,
1606 alias_occurrence,
1607 lexical_scopes,
1608 );
1609 let id = SourceImportId::try_from_index(self.source_imports.len())
1610 .expect("Python source import ids must fit in a u32");
1611 self.source_imports.push(source_import);
1612 self.generic_imports.push(id);
1613 }
1614 }
1615
1616 fn finish(
1617 self,
1618 root: Node<'_>,
1619 native_step: impl FnOnce(
1620 &mut ParsedFile,
1621 &mut PrimarySourceFactCollector<'source>,
1622 &[(SourceDeclarationId, CodeUnit)],
1623 ) -> NativeSourceBridges,
1624 ) {
1625 let PythonVisitor {
1626 parsed,
1627 source,
1628 mut source_facts,
1629 structural,
1630 structural_parents,
1631 source_imports,
1632 generic_imports,
1633 source_declaration_units,
1634 python_source_facts,
1635 python_callable_return_by_node: _,
1636 overload_decorators,
1637 overload_metadata,
1638 ..
1639 } = self;
1640 assert_eq!(structural_parents, vec![None]);
1641 let structural = structural
1642 .finish(root)
1643 .expect("Python structural fact collection must finish");
1644 let (native_site_occurrences, native_declaration_sources) =
1645 native_step(parsed, &mut source_facts, &source_declaration_units);
1646 let occurrences = source_facts.finish();
1647 assert!(
1648 python_source_facts.valid_links(&occurrences),
1649 "Python callable return facts must link to exact source identities"
1650 );
1651 for (unit, metadata_ordinal, decorators) in overload_metadata {
1652 let declaration_only = decorators
1653 .iter()
1654 .any(|name| overload_decorators.matches_overload_binding(name));
1655 if declaration_only
1656 && let Some(metadata) = parsed
1657 .signature_metadata
1658 .get_mut(&unit)
1659 .and_then(|entries| entries.get_mut(metadata_ordinal))
1660 {
1661 *metadata = metadata.clone().with_declaration_only(true);
1662 }
1663 }
1664 parsed.imports = generic_imports
1665 .iter()
1666 .map(|id| source_imports[id.index()].import_info(&occurrences))
1667 .collect();
1668 parsed.source_declaration_units = source_declaration_units;
1669 parsed.source_facts = Some(ParsedSourceFacts {
1670 cpp: None,
1671 go: None,
1672 java: None,
1673 js_ts: None,
1674 php: None,
1675 scala: None,
1676 ruby: None,
1677 source_bytes: source.len(),
1678 occurrences,
1679 structural,
1680 python: Some(python_source_facts),
1681 native_site_occurrences,
1682 native_declaration_sources,
1683 declaration_visibilities: None,
1684 rust_declaration_properties: Vec::new(),
1685 rust_modules: None,
1686 rust_types: Vec::new(),
1687 rust_items: RustItemSourceFacts::default(),
1688 imports: source_imports,
1689 generic_imports,
1690 rust_import_contexts: Vec::new(),
1691 });
1692 }
1693}
1694
1695type NativeSourceBridges = (
1696 Vec<SourceOccurrenceId>,
1697 Vec<(ResolutionSiteId, SourceDeclarationId)>,
1698);
1699
1700pub fn parse_python_file(
1703 project: &dyn Project,
1704 file: &ProjectFile,
1705 source: &str,
1706 tree: &Tree,
1707) -> ParsedFile {
1708 parse_python_file_with_step(project, file, source, tree, |_, _, _| Default::default())
1709}
1710
1711#[cfg(any(test, feature = "test-support"))]
1714pub fn parse_python_file_with_native_facts(
1715 project: &dyn Project,
1716 file: &ProjectFile,
1717 source: &str,
1718 tree: &Tree,
1719) -> ParsedFile {
1720 parse_python_file_with_step(project, file, source, tree, |parsed, collector, units| {
1721 let (facts, occurrences, declarations) = crate::resolution::produce(
1724 tree.root_node(),
1725 source,
1726 collector,
1727 units
1728 .iter()
1729 .filter(|(_, unit)| parsed.contains_declaration(unit)),
1730 );
1731 parsed.resolution_facts = facts;
1732 (occurrences, declarations)
1733 })
1734}
1735
1736fn parse_python_file_with_step<'source>(
1737 project: &dyn Project,
1738 file: &ProjectFile,
1739 source: &'source str,
1740 tree: &Tree,
1741 native_step: impl FnOnce(
1742 &mut ParsedFile,
1743 &mut PrimarySourceFactCollector<'source>,
1744 &[(SourceDeclarationId, CodeUnit)],
1745 ) -> NativeSourceBridges,
1746) -> ParsedFile {
1747 let module_components = python_module_components(project, file);
1748 let module_name = module_components.join(".");
1749 let module_fq = FqName::from_package_components(&module_components);
1750 let mut parsed = ParsedFile::new(module_name.clone());
1751 let root = tree.root_node();
1752
1753 let module_code_unit = module_code_unit_from_fq(file, &module_components, module_fq.clone());
1754 if let Some(module) = module_code_unit.clone() {
1755 parsed.add_code_unit(module, root, source, None, None);
1756 }
1757
1758 let overload_decorators = PythonOverloadDecoratorBindings::default();
1759 let structural_kinds = CompiledKinds::compile(
1760 &tree_sitter_python::LANGUAGE.into(),
1761 PYTHON_STRUCTURAL_SPEC.kind_table(),
1762 );
1763 let call_site_context = PYTHON_STRUCTURAL_SPEC.call_site_context(root, source);
1764 let mut visitor = PythonVisitor {
1765 file,
1766 source,
1767 package_name: &module_name,
1768 module_fq: &module_fq,
1769 parsed: &mut parsed,
1770 module: module_code_unit,
1771 overload_decorators,
1772 source_facts: PrimarySourceFactCollector::new(source),
1773 structural: StructuralFactCollector::new(
1774 &PYTHON_STRUCTURAL_SPEC,
1775 source,
1776 &call_site_context,
1777 ParentIndex::new(root),
1778 usize::MAX,
1779 None,
1780 ),
1781 structural_kinds: &structural_kinds,
1782 call_site_context: &call_site_context,
1783 structural_parents: vec![None],
1784 source_imports: Vec::new(),
1785 generic_imports: Vec::new(),
1786 source_declaration_units: Vec::new(),
1787 source_declaration_unit_keys: HashSet::default(),
1788 python_source_facts: PythonSourceFacts::default(),
1789 python_callable_return_by_node: HashMap::default(),
1790 overload_metadata: Vec::new(),
1791 };
1792 visitor.visit_container(root, &[], 0);
1793 visitor.finish(root, native_step);
1794
1795 parsed
1796}
1797
1798pub use brokk_bifrost_core::analyzer::common::node_source_text as py_node_text;
1799
1800pub fn python_module_name(project: &dyn Project, file: &ProjectFile) -> String {
1801 python_module_components(project, file).join(".")
1802}
1803
1804pub fn python_module_components_with_inputs(
1807 file: &ProjectFile,
1808 inputs: &impl PythonImportRootInputs,
1809) -> std::io::Result<Vec<String>> {
1810 let root = python_import_root_with_inputs(file.root(), file.rel_path(), inputs)?;
1811 Ok(python_module_components_from_root(file, &root))
1812}
1813
1814pub fn module_code_unit_from_name(file: &ProjectFile, name: &str) -> Option<CodeUnit> {
1816 let components = python_module_components_from_name(file, name)?;
1817 module_code_unit_from_components(file, &components)
1818}
1819
1820pub fn module_code_unit(project: &dyn Project, file: &ProjectFile) -> Option<CodeUnit> {
1821 let components = python_module_components(project, file);
1822 module_code_unit_from_components(file, &components)
1823}
1824
1825pub fn module_code_unit_from_components(
1827 file: &ProjectFile,
1828 components: &[String],
1829) -> Option<CodeUnit> {
1830 module_code_unit_from_fq(
1831 file,
1832 components,
1833 FqName::from_package_components(components),
1834 )
1835}
1836
1837fn module_code_unit_from_fq(
1838 file: &ProjectFile,
1839 components: &[String],
1840 structured_fq: FqName,
1841) -> Option<CodeUnit> {
1842 let (short_name, package_components) = components.split_last()?;
1843 let package_name = package_components.join(".");
1844 Some(CodeUnit::new_fq(
1845 file.clone(),
1846 CodeUnitType::Module,
1847 package_name,
1848 short_name.clone(),
1849 structured_fq,
1850 ))
1851}
1852
1853fn python_class_signature(node: Node<'_>, source: &str) -> String {
1854 python_header_with_decorators(node, source)
1855}
1856
1857fn python_function_signature(function: Node<'_>, source: &str) -> String {
1858 let header_end = (0..function.child_count())
1859 .rev()
1860 .filter_map(|index| function.child(index))
1861 .find(|child| child.kind() == ":")
1862 .map_or_else(
1863 || {
1864 function
1865 .child_by_field_name("body")
1866 .map_or(function.end_byte(), |body| body.start_byte())
1867 },
1868 |colon| colon.end_byte(),
1869 );
1870 let header_node = python_function_header_node(function);
1871 let header_start = header_node.start_byte();
1872 let header = source
1873 .get(header_start..header_end)
1874 .expect("Python function header must be a source range")
1875 .trim_end();
1876 format!(
1877 "{} ...",
1878 python_dedent_function_header(header, header_node.start_position().column)
1879 )
1880}
1881
1882fn python_dedent_function_header(header: &str, base_indent: usize) -> String {
1883 let mut lines = header.lines();
1884 let Some(first) = lines.next() else {
1885 return String::new();
1886 };
1887 let mut dedented = String::with_capacity(header.len());
1888 dedented.push_str(first);
1889 for line in lines {
1890 dedented.push('\n');
1891 dedented.push_str(&line[python_header_indent_bytes(line, base_indent)..]);
1892 }
1893 dedented
1894}
1895
1896fn python_dedented_header_offset(source_prefix: &str, base_indent: usize) -> usize {
1899 let mut removed_bytes = 0;
1900 let mut line_start = 0;
1901 let mut continuation_line = false;
1902
1903 while line_start < source_prefix.len() {
1904 let newline = source_prefix.as_bytes()[line_start..]
1905 .iter()
1906 .position(|byte| *byte == b'\n')
1907 .map(|offset| line_start + offset);
1908 let line_end = newline.unwrap_or(source_prefix.len());
1909 let line = &source_prefix[line_start..line_end];
1910 if continuation_line {
1911 removed_bytes += python_header_indent_bytes(line, base_indent);
1912 }
1913 let Some(newline) = newline else {
1914 break;
1915 };
1916 if newline > 0 && source_prefix.as_bytes()[newline - 1] == b'\r' {
1917 removed_bytes += 1;
1918 }
1919 line_start = newline + 1;
1920 continuation_line = true;
1921 }
1922
1923 source_prefix.len() - removed_bytes
1924}
1925
1926fn python_header_indent_bytes(line: &str, base_indent: usize) -> usize {
1927 line.bytes()
1928 .take_while(|byte| matches!(*byte, b' ' | b'\t'))
1929 .count()
1930 .min(base_indent)
1931}
1932
1933fn python_signature_metadata(
1934 signature: String,
1935 node: Node<'_>,
1936 source: &str,
1937 return_type_text: Option<&str>,
1938 return_type_identity: Option<StructuredTypeIdentity>,
1939) -> SignatureMetadata {
1940 let Some(parameters_node) = node.child_by_field_name("parameters") else {
1941 return SignatureMetadata::new(signature, Vec::new())
1942 .with_return_type_text(return_type_text)
1943 .with_return_type_identity(return_type_identity)
1944 .with_dispatch_extensibility(DispatchExtensibility::Open)
1945 .with_callable_modifiers(
1946 python_callable_is_static(node, source),
1947 false,
1948 DeclaredVisibility::Unknown,
1949 );
1950 };
1951 let arity = python_callable_arity(parameters_node, source);
1952 let header_node = python_function_header_node(node);
1957 let header_start = header_node.start_byte();
1958 let base_indent = header_node.start_position().column;
1959 let parameters = python_parameter_label_nodes(parameters_node)
1960 .into_iter()
1961 .map(|label_node| {
1962 let label = py_node_text(label_node, source);
1963 let label_range = label_node.byte_range();
1964 let source_prefix = source
1965 .get(header_start..label_range.start)
1966 .expect("Python parameter must follow its function header");
1967 let start_byte = python_dedented_header_offset(source_prefix, base_indent);
1968 let end_byte = start_byte + label.len();
1969 assert_eq!(
1970 signature.get(start_byte..end_byte),
1971 Some(label),
1972 "Python parameter metadata must point at its exact signature label"
1973 );
1974 ParameterMetadata::new(label, start_byte, end_byte)
1975 })
1976 .collect();
1977 let metadata = SignatureMetadata::new(signature, parameters)
1978 .with_return_type_text(return_type_text)
1979 .with_return_type_identity(return_type_identity)
1980 .with_dispatch_extensibility(DispatchExtensibility::Open)
1981 .with_callable_modifiers(
1982 python_callable_is_static(node, source),
1983 false,
1984 DeclaredVisibility::Unknown,
1985 );
1986 if let Some(arity) = arity {
1987 metadata.with_callable_arity(arity)
1988 } else {
1989 metadata
1990 }
1991}
1992
1993fn python_callable_arity(parameters: Node<'_>, source: &str) -> Option<CallableArity> {
1998 let mut required = 0;
1999 let mut total = 0;
2000 let mut repeated = false;
2001 let mut keyword_only = false;
2002 let mut cursor = parameters.walk();
2003 for parameter in parameters.named_children(&mut cursor) {
2004 match parameter.kind() {
2005 "positional_separator" => continue,
2006 "keyword_separator" => {
2007 keyword_only = true;
2008 continue;
2009 }
2010 "ERROR" => return None,
2011 _ => {}
2012 }
2013 if !matches!(
2014 parameter.kind(),
2015 "identifier"
2016 | "typed_parameter"
2017 | "default_parameter"
2018 | "typed_default_parameter"
2019 | "list_splat_pattern"
2020 | "dictionary_splat_pattern"
2021 ) {
2022 continue;
2023 }
2024 let label = python_parameter_label_node(parameter)?;
2025 source.get(label.byte_range())?;
2026 match python_parameter_splat(parameter) {
2027 Some(PythonParameterSplat::Positional) => {
2028 repeated = true;
2029 keyword_only = true;
2030 }
2031 Some(PythonParameterSplat::Keyword) => {}
2032 None if !keyword_only => {
2033 total += 1;
2034 if !matches!(
2035 parameter.kind(),
2036 "default_parameter" | "typed_default_parameter"
2037 ) {
2038 required += 1;
2039 }
2040 }
2041 None => {}
2042 }
2043 }
2044 Some(CallableArity::new(required, total, repeated))
2045}
2046
2047fn python_function_header_node(node: Node<'_>) -> Node<'_> {
2048 node.parent()
2049 .filter(|parent| parent.kind() == "decorated_definition")
2050 .unwrap_or(node)
2051}
2052
2053fn python_callable_is_static(node: Node<'_>, source: &str) -> bool {
2078 debug_assert!(
2079 node.kind() == "function_definition",
2080 "Python callable signature metadata is built for function_definition, not {}",
2081 node.kind()
2082 );
2083 python_function_has_decorator(node, source, "staticmethod")
2084 || python_function_has_decorator(node, source, "classmethod")
2085}
2086
2087fn python_parameter_label_nodes(parameters_node: Node<'_>) -> Vec<Node<'_>> {
2088 brokk_bifrost_core::analyzer::tree_walk::named_children_iter(parameters_node)
2089 .filter_map(python_parameter_label_node)
2090 .collect()
2091}
2092
2093#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2110pub enum PythonParameterSplat {
2111 Positional,
2113 Keyword,
2115}
2116
2117pub fn python_parameter_splat(parameter: Node<'_>) -> Option<PythonParameterSplat> {
2118 let splat = match parameter.kind() {
2119 kind @ ("list_splat_pattern" | "dictionary_splat_pattern") => kind,
2120 _ => {
2121 let mut cursor = parameter.walk();
2122 parameter
2123 .named_children(&mut cursor)
2124 .map(|child| child.kind())
2125 .find(|kind| matches!(*kind, "list_splat_pattern" | "dictionary_splat_pattern"))?
2126 }
2127 };
2128 match splat {
2129 "list_splat_pattern" => Some(PythonParameterSplat::Positional),
2130 "dictionary_splat_pattern" => Some(PythonParameterSplat::Keyword),
2131 _ => unreachable!("the splat kind was matched above"),
2132 }
2133}
2134
2135pub fn python_parameter_label_node(node: Node<'_>) -> Option<Node<'_>> {
2136 match node.kind() {
2137 "identifier" => Some(node),
2138 "typed_parameter"
2139 | "typed_default_parameter"
2140 | "default_parameter"
2141 | "list_splat_pattern"
2142 | "dictionary_splat_pattern"
2143 | "keyword_separator" => node.child_by_field_name("name").or_else(|| {
2144 let mut cursor = node.walk();
2145 node.named_children(&mut cursor)
2146 .find_map(python_parameter_label_node)
2147 }),
2148 _ => None,
2149 }
2150}
2151
2152fn python_is_property_mutator(node: Node<'_>, source: &str) -> bool {
2153 python_header_with_decorators(node, source)
2154 .lines()
2155 .map(str::trim)
2156 .filter(|line| line.starts_with('@'))
2157 .any(|decorator| decorator.ends_with(".setter") || decorator.ends_with(".deleter"))
2158}
2159
2160pub fn python_expanded_comment_start(source: &str, start_byte: usize) -> usize {
2161 let line_starts = compute_line_starts(source);
2162 let line_index = find_line_index_for_offset(&line_starts, start_byte);
2163
2164 let mut comment_start = start_byte;
2165 for line_idx in (0..line_index).rev() {
2166 let line_start = line_starts[line_idx];
2167 let line_end = line_starts
2168 .get(line_idx + 1)
2169 .copied()
2170 .unwrap_or(source.len());
2171 let line = &source[line_start..line_end];
2172 let trimmed = line.trim_start();
2173
2174 if trimmed.trim().is_empty() {
2175 continue;
2176 }
2177
2178 if trimmed.starts_with('#') {
2179 comment_start = line_start;
2180 continue;
2181 }
2182
2183 break;
2184 }
2185
2186 comment_start
2187}
2188
2189fn python_header_with_decorators(node: Node<'_>, source: &str) -> String {
2190 let raw = py_node_text(node, source);
2191 let lines: Vec<_> = raw
2192 .lines()
2193 .map(str::trim_end)
2194 .filter(|line| !line.trim().is_empty())
2195 .collect();
2196 let mut relevant = Vec::new();
2197 for line in lines {
2198 let trimmed = line.trim_start();
2199 if trimmed.starts_with('@')
2200 || trimmed.starts_with("def ")
2201 || trimmed.starts_with("async def ")
2202 || trimmed.starts_with("class ")
2203 {
2204 relevant.push(trimmed.to_string());
2205 if trimmed.starts_with("def ")
2206 || trimmed.starts_with("async def ")
2207 || trimmed.starts_with("class ")
2208 {
2209 break;
2210 }
2211 }
2212 }
2213 relevant.join("\n")
2214}
2215
2216fn extract_python_supertypes(node: Node<'_>, source: &str) -> Vec<String> {
2235 let Some(superclasses) = node.child_by_field_name("superclasses") else {
2236 return Vec::new();
2237 };
2238 let mut result = Vec::new();
2239 let mut cursor = superclasses.walk();
2240 for child in superclasses.named_children(&mut cursor) {
2241 if child.kind() == "keyword_argument" {
2242 continue;
2243 }
2244 let named = python_base_origin_node(child);
2245 let text = py_node_text(named, source).trim();
2246 if !text.is_empty() {
2247 result.push(text.to_string());
2248 }
2249 }
2250 result
2251}
2252
2253pub fn python_base_origin_node<'tree>(base: Node<'tree>) -> Node<'tree> {
2256 if base.kind() != "subscript" {
2257 return base;
2258 }
2259 let Some(value) = base.child_by_field_name("value") else {
2260 return base;
2261 };
2262 if matches!(value.kind(), "identifier" | "attribute") {
2263 value
2264 } else {
2265 base
2266 }
2267}
2268
2269fn collect_assigned_name_nodes<'tree>(node: Node<'tree>, _source: &str) -> Vec<Node<'tree>> {
2270 let mut names = Vec::new();
2271 walk_named_tree_preorder(node, true, |node| {
2272 match node.kind() {
2273 "attribute" | "subscript" => WalkControl::SkipChildren,
2277 "identifier" => {
2278 names.push(node);
2279 WalkControl::Continue
2280 }
2281 _ => WalkControl::Continue,
2282 }
2283 });
2284 names
2285}
2286
2287fn collect_self_assigned_attributes<'tree>(
2288 node: Node<'tree>,
2289 source: &str,
2290 receiver_name: &str,
2291) -> Vec<(String, Node<'tree>)> {
2292 let mut attributes = Vec::new();
2293 collect_direct_self_assigned_attributes(node, source, receiver_name, &mut attributes);
2294 attributes
2295}
2296
2297fn collect_direct_self_assigned_attributes<'tree>(
2298 node: Node<'tree>,
2299 source: &str,
2300 receiver_name: &str,
2301 attributes: &mut Vec<(String, Node<'tree>)>,
2302) {
2303 let mut pending = vec![node];
2304 while let Some(node) = pending.pop() {
2305 match node.kind() {
2306 "attribute" => {
2307 let Some(object) = node.child_by_field_name("object") else {
2308 continue;
2309 };
2310 if object.kind() != "identifier"
2311 || py_node_text(object, source).trim() != receiver_name
2312 {
2313 continue;
2314 }
2315 let Some(attribute) = node.child_by_field_name("attribute") else {
2316 continue;
2317 };
2318 let name = py_node_text(attribute, source).trim();
2319 if !name.is_empty() {
2320 attributes.push((name.to_string(), attribute));
2321 }
2322 }
2323 "pattern_list"
2328 | "tuple_pattern"
2329 | "list_pattern"
2330 | "tuple"
2331 | "list"
2332 | "parenthesized_expression" => {
2333 let mut cursor = node.walk();
2334 let children: Vec<_> = node.named_children(&mut cursor).collect();
2335 pending.extend(children.into_iter().rev());
2336 }
2337 _ => {}
2338 }
2339 }
2340}
2341
2342fn python_instance_method_receiver_name(node: Node<'_>, source: &str) -> Option<String> {
2343 if python_function_has_decorator(node, source, "staticmethod")
2344 || python_function_has_decorator(node, source, "classmethod")
2345 {
2346 return None;
2347 }
2348 python_first_parameter_name(node, source)
2349}
2350
2351pub fn python_function_has_decorator(node: Node<'_>, source: &str, decorator_name: &str) -> bool {
2352 let Some(parent) = node.parent() else {
2353 return false;
2354 };
2355 if parent.kind() != "decorated_definition" {
2356 return false;
2357 }
2358 let mut cursor = parent.walk();
2359 parent
2360 .named_children(&mut cursor)
2361 .filter(|child| child.kind() == "decorator")
2362 .filter_map(|decorator| decorator.named_child(0))
2363 .filter_map(expression_name_node)
2364 .any(|name| py_node_text(name, source).trim() == decorator_name)
2365}
2366
2367pub fn python_function_has_property_decorator(node: Node<'_>, source: &str) -> bool {
2375 python_function_has_decorator(node, source, "property")
2376 || python_function_has_decorator(node, source, "cached_property")
2377}
2378
2379pub fn python_first_parameter_name(node: Node<'_>, source: &str) -> Option<String> {
2382 let parameters = node.child_by_field_name("parameters")?;
2383 let mut cursor = parameters.walk();
2384 parameters
2385 .named_children(&mut cursor)
2386 .find_map(|child| python_parameter_name(child, source))
2387}
2388
2389pub fn collect_python_identifiers(node: Node<'_>, source: &str, identifiers: &mut HashSet<String>) {
2390 brokk_bifrost_core::analyzer::tree_walk::collect_trimmed_node_texts(
2391 node,
2392 source,
2393 identifiers,
2394 |node| node.kind() == "identifier",
2395 );
2396}
2397
2398pub fn parse_python_tree(source: &str) -> Option<Tree> {
2399 let mut parser = Parser::new();
2400 parser
2401 .set_language(&tree_sitter_python::LANGUAGE.into())
2402 .expect("failed to load python parser");
2403 parser.parse(source, None)
2404}
2405
2406#[cfg(test)]
2407mod primary_tests {
2408 use super::*;
2409 use brokk_bifrost_core::analyzer::python_facts::PythonAnnotationReferenceName;
2410
2411 fn parse(source: &str) -> ParsedFile {
2412 let file = ProjectFile::new(std::env::temp_dir(), "m4.py");
2413 let tree = parse_python_tree(source).expect("Python fixture tree");
2414 parse_python_file(
2415 &brokk_bifrost_core::analyzer::project::TestProject::new(
2416 file.root(),
2417 brokk_bifrost_core::analyzer::Language::Python,
2418 ),
2419 &file,
2420 source,
2421 &tree,
2422 )
2423 }
2424
2425 fn parse_native(source: &str) -> ParsedFile {
2426 let file = ProjectFile::new(std::env::temp_dir(), "m4.py");
2427 let tree = parse_python_tree(source).expect("Python fixture tree");
2428 parse_python_file_with_native_facts(
2429 &brokk_bifrost_core::analyzer::project::TestProject::new(
2430 file.root(),
2431 brokk_bifrost_core::analyzer::Language::Python,
2432 ),
2433 &file,
2434 source,
2435 &tree,
2436 )
2437 }
2438
2439 #[test]
2440 #[brokk_bifrost_test_macros::hardwon(
2441 "6b123df7cc68: quicksilver round 6 on brokk-bifrost-python: nested assignment targets recursively exhausted the stack"
2442 )]
2443 fn qs6_nested_unpacking_is_stack_safe() {
2444 let depth = 1024;
2445 let source = format!(
2446 "{}self.first, self.last{} = pair\n",
2447 "[".repeat(depth),
2448 "]".repeat(depth)
2449 );
2450 let tree = parse_python_tree(&source).unwrap();
2451 assert!(!tree.root_node().has_error());
2452 let assignment = tree
2453 .root_node()
2454 .named_child(0)
2455 .unwrap()
2456 .named_child(0)
2457 .unwrap();
2458 let target = assignment.child_by_field_name("left").unwrap();
2459 let attributes = std::thread::scope(|scope| {
2460 std::thread::Builder::new()
2461 .stack_size(64 * 1024)
2462 .spawn_scoped(scope, || {
2463 collect_self_assigned_attributes(target, &source, "self")
2464 })
2465 .unwrap()
2466 .join()
2467 .unwrap()
2468 });
2469 assert_eq!(
2470 attributes
2471 .iter()
2472 .map(|(name, _)| name.as_str())
2473 .collect::<Vec<_>>(),
2474 ["first", "last"]
2475 );
2476 }
2477
2478 #[test]
2479 fn qs6_setuptools_literal_dependency_boundary() {
2480 let source = "from setuptools import setup\nsetup(install_requires=('requests>=2',), setup_requires=['wheel'], tests_require=('pytest',), extras_require={'speed': ('orjson',)})\nsetup(install_requires=dynamic())\n";
2483 let arguments = setuptools_setup_py_literal_arguments(source).unwrap();
2484 let rendered = arguments
2485 .iter()
2486 .map(|argument| {
2487 format!(
2488 "{} {} {:?} {}",
2489 argument.call_index,
2490 argument.name,
2491 argument.value,
2492 &source[argument.byte_range.clone()]
2493 )
2494 })
2495 .collect::<Vec<_>>()
2496 .join("\n");
2497 assert_eq!(
2498 rendered,
2499 concat!(
2500 "0 install_requires Some(Strings([\"requests>=2\"])) ('requests>=2',)\n",
2501 "0 setup_requires Some(Strings([\"wheel\"])) ['wheel']\n",
2502 "0 tests_require Some(Strings([\"pytest\"])) ('pytest',)\n",
2503 "0 extras_require Some(Extras([(\"speed\", [\"orjson\"])])) {'speed': ('orjson',)}\n",
2504 "1 install_requires None dynamic()",
2505 )
2506 );
2507 }
2508
2509 #[test]
2510 #[brokk_bifrost_test_macros::hardwon(
2511 "#3673: declarations ignored an overlay-only setuptools manifest"
2512 )]
2513 fn snapshot_root_declaration_witness() {
2514 use brokk_bifrost_core::analyzer::Language;
2515 use brokk_bifrost_core::analyzer::project::{OverlayProject, Project, TestProject};
2516 use std::sync::Arc;
2517 let root = tempfile::tempdir().unwrap();
2518 let disk = Arc::new(TestProject::new(root.path(), Language::Python));
2519 let overlay = OverlayProject::new(disk);
2520 let manifest = ProjectFile::new(root.path(), "pyproject.toml");
2521 assert!(overlay.set(
2522 manifest.abs_path(),
2523 "[tool.setuptools.packages.find]\nwhere = ['source']\n".to_string()
2524 ));
2525 let file = ProjectFile::new(root.path(), "source/pkg/model.py");
2526 assert!(overlay.set(file.abs_path(), "class Widget:\n pass\n".to_string()));
2527 let snapshot = overlay.snapshot();
2528 assert!(snapshot.read_source(&manifest).unwrap().contains("source"));
2529 let source = snapshot.read_source(&file).unwrap();
2530 let tree = parse_python_tree(&source).unwrap();
2531 let parsed = parse_python_file(&snapshot, &file, &source, &tree);
2532 let names: Vec<_> = parsed
2533 .declarations()
2534 .iter()
2535 .map(CodeUnit::fq_name)
2536 .collect();
2537 assert!(names.contains(&"pkg.model.Widget".to_string()), "{names:?}");
2538 }
2539
2540 #[test]
2541 fn setup_py_unwrap_parenthesized_skips_comments() {
2542 let source = "VALUE = (\n # before\n \"value\" # after\n)\n";
2543 let tree = parse_python_tree(source).expect("Python fixture tree");
2544 let mut pending = vec![tree.root_node()];
2545 let parenthesized = loop {
2546 let node = pending.pop().expect("parenthesized expression exists");
2547 if node.kind() == "parenthesized_expression" {
2548 break node;
2549 }
2550 let mut cursor = node.walk();
2551 pending.extend(node.named_children(&mut cursor));
2552 };
2553
2554 assert_eq!(
2555 setup_py_unwrap_parenthesized(parenthesized).kind(),
2556 "string"
2557 );
2558 }
2559
2560 #[test]
2561 #[brokk_bifrost_test_macros::hardwon(
2562 "f4495e5c7c62: replacing a duplicate Python class left stale descendant callable metadata links"
2563 )]
2564 fn replacing_a_class_withdraws_descendant_metadata_links() {
2565 let parsed = parse_native(
2566 "from typing import overload\nclass Replaced:\n def obsolete(self): pass\n @overload\n def current(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",
2567 );
2568 for bridge in &parsed.resolution_facts.definition_units {
2569 assert!(
2570 parsed.contains_declaration(&bridge.unit),
2571 "native definitions must not resurrect withdrawn descendants: {bridge:?}"
2572 );
2573 }
2574 let mut names = parsed
2575 .source_declaration_metadata
2576 .iter()
2577 .map(|link| {
2578 assert!(
2579 parsed.signature_metadata[&link.unit]
2580 .get(link.metadata_ordinal)
2581 .is_some(),
2582 "the link must name retained callable metadata: {link:?}"
2583 );
2584 link.unit.short_name().to_string()
2585 })
2586 .collect::<Vec<_>>();
2587 names.sort();
2588 assert_eq!(names, ["Kept.stable", "Replaced.current"]);
2589 let current = parsed
2590 .source_declaration_metadata
2591 .iter()
2592 .find(|link| link.unit.short_name() == "Replaced.current")
2593 .unwrap();
2594 assert!(
2595 !parsed.signature_metadata[¤t.unit][current.metadata_ordinal]
2596 .is_declaration_only(),
2597 "a withdrawn overload must not mark the new implementation"
2598 );
2599 assert_eq!(
2600 parsed
2601 .source_declaration_units
2602 .iter()
2603 .filter(|(_, unit)| unit.short_name() == "Replaced.current")
2604 .count(),
2605 1,
2606 "the new method replaces even an already-withdrawn source link"
2607 );
2608 }
2609
2610 #[test]
2611 fn canonical_declarations_share_structural_name_identity_and_exact_ranges() {
2612 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";
2613 let parsed = parse(source);
2614 let facts = parsed
2615 .source_facts
2616 .as_ref()
2617 .expect("canonical Python facts");
2618 for (id, unit) in &parsed.source_declaration_units {
2619 let declaration = facts.occurrences.declaration(*id);
2620 let range = facts.occurrences.occurrence(declaration.occurrence).range;
2621 assert!(parsed.ranges[unit].contains(&range), "{unit:?}: {range:?}");
2622 let name = declaration.name.expect("source declaration name");
2623 assert!(
2624 facts
2625 .structural
2626 .nodes()
2627 .iter()
2628 .any(|node| node.occurrence == name),
2629 "shared name identity for {unit:?}"
2630 );
2631 }
2632 let fields = parsed
2633 .source_declaration_units
2634 .iter()
2635 .filter(|(_, unit)| unit.short_name() == "Item.value")
2636 .collect::<Vec<_>>();
2637 assert_eq!(fields.len(), 2);
2638 assert_ne!(fields[0].0, fields[1].0);
2639 assert_eq!(
2640 parsed
2641 .source_declaration_units
2642 .iter()
2643 .filter(|(_, unit)| unit.short_name() == "Item.member")
2644 .count(),
2645 1
2646 );
2647 let class = parsed
2648 .source_declaration_units
2649 .iter()
2650 .find(|(_, unit)| unit.short_name() == "Item")
2651 .expect("decorated class");
2652 let range = facts
2653 .occurrences
2654 .occurrence(facts.occurrences.declaration(class.0).occurrence)
2655 .range;
2656 assert_eq!(range.start_byte, 0);
2657 assert_eq!(&source[range.start_byte..range.start_byte + 9], "@decorate");
2658 }
2659
2660 #[test]
2661 fn imports_keep_scope_order_and_late_overload_capture_without_local_pollution() {
2662 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";
2663 let parsed = parse(source);
2664 let facts = parsed.source_facts.as_ref().expect("canonical imports");
2665 assert_eq!(facts.imports.len(), 3);
2666 assert!(
2667 facts.imports[0]
2668 .path
2669 .as_ref()
2670 .unwrap()
2671 .lexical_scopes
2672 .is_empty()
2673 );
2674 assert_eq!(
2675 facts.imports[1].path.as_ref().unwrap().lexical_scopes.len(),
2676 1
2677 );
2678 assert!(
2679 facts.imports[2]
2680 .path
2681 .as_ref()
2682 .unwrap()
2683 .lexical_scopes
2684 .is_empty()
2685 );
2686 for (id, import) in facts.generic_imports.iter().zip(&parsed.imports) {
2687 assert_eq!(
2688 &facts.imports[id.index()].import_info(&facts.occurrences),
2689 import
2690 );
2691 }
2692 let metadata = |name: &str| {
2693 parsed
2694 .signature_metadata
2695 .iter()
2696 .find(|(unit, _)| unit.short_name() == name)
2697 .unwrap()
2698 .1
2699 };
2700 assert!(metadata("public")[0].is_declaration_only());
2701 assert!(!metadata("ordinary")[0].is_declaration_only());
2702 }
2703
2704 #[test]
2705 #[brokk_bifrost_test_macros::hardwon(
2706 "1fa35d421feb: aliased import leaves pointed at imported-name ranges instead of the whole import statement"
2707 )]
2708 fn aliased_import_leaves_keep_the_statement_and_exact_binder_tokens() {
2709 let source = "from pkg import alpha as beta, beta as alpha\n";
2710 let parsed = parse(source);
2711 assert_eq!(parsed.imports.len(), 2);
2712 for (import, name) in parsed.imports.iter().zip(["beta", "alpha"]) {
2713 assert_eq!(import.path.as_ref().unwrap().declaration_start_byte, 0);
2714 let span = import.binder_span.as_ref().expect("exact alias binder");
2715 assert_eq!(&source[span.start_byte..span.end_byte], name);
2716 }
2717 }
2718
2719 #[test]
2720 fn parameter_offsets_use_ast_tokens_instead_of_annotation_spelling() {
2721 let source = "@decorate(\"(first, second)\")\ndef call(first: Literal[\"second\"], second: int):\n pass\n";
2722 let parsed = parse(source);
2723 let metadata = parsed
2724 .signature_metadata
2725 .values()
2726 .flatten()
2727 .find(|metadata| metadata.parameters().len() == 2)
2728 .expect("parameter metadata");
2729 let second = &metadata.parameters()[1];
2730 assert_eq!(
2731 &metadata.label()[second.start_byte()..second.end_byte()],
2732 "second"
2733 );
2734 assert_eq!(
2735 second.start_byte(),
2736 metadata.label().rfind("second:").unwrap()
2737 );
2738 }
2739
2740 #[test]
2741 fn multiline_parameter_metadata_keeps_each_formal_name_at_its_ast_offset() {
2742 let source = concat!(
2743 "@decorate\n",
2744 "def call(\n",
2745 " bare,\n",
2746 " typed: T,\n",
2747 " default=1,\n",
2748 " typed_default: U = 2,\n",
2749 " *args: V,\n",
2750 " required_kw,\n",
2751 " **kwargs: W,\n",
2752 "):\n",
2753 " pass\n",
2754 );
2755 let parsed = parse(source);
2756 let metadata = parsed
2757 .signature_metadata
2758 .values()
2759 .flatten()
2760 .find(|metadata| metadata.parameters().len() == 7)
2761 .expect("multiline callable parameter metadata");
2762 let names = metadata
2763 .parameters()
2764 .iter()
2765 .map(|parameter| &metadata.label()[parameter.start_byte()..parameter.end_byte()])
2766 .collect::<Vec<_>>();
2767 assert_eq!(
2768 names,
2769 [
2770 "bare",
2771 "typed",
2772 "default",
2773 "typed_default",
2774 "args",
2775 "required_kw",
2776 "kwargs"
2777 ]
2778 );
2779 assert!(metadata.label().contains(" typed_default: U = 2,"));
2780 assert!(metadata.label().ends_with(" ..."));
2781 }
2782
2783 #[test]
2784 fn crlf_multiline_parameter_metadata_keeps_each_formal_name_at_its_ast_offset() {
2785 let source = concat!(
2786 "@decorate\r\n",
2787 "def call(\r\n",
2788 " bare,\r\n",
2789 " typed: T,\r\n",
2790 " default=1,\r\n",
2791 "):\r\n",
2792 " pass\r\n",
2793 );
2794 let parsed = parse(source);
2795 let metadata = parsed
2796 .signature_metadata
2797 .values()
2798 .flatten()
2799 .find(|metadata| metadata.parameters().len() == 3)
2800 .expect("multiline callable parameter metadata");
2801 let names = metadata
2802 .parameters()
2803 .iter()
2804 .map(|parameter| &metadata.label()[parameter.start_byte()..parameter.end_byte()])
2805 .collect::<Vec<_>>();
2806 assert_eq!(names, ["bare", "typed", "default"]);
2807 }
2808
2809 #[test]
2810 fn crlf_parameter_metadata_preserves_normalized_signature_offsets() {
2811 for indentation in ["", " "] {
2812 let header = concat!(
2813 "@decorate\r\n",
2814 "def call(\r\n",
2815 " first: T,\r\n",
2816 " second: U = 2,\r\n",
2817 "):\r\n",
2818 " pass\r\n",
2819 );
2820 let source = if indentation.is_empty() {
2821 header.to_owned()
2822 } else {
2823 format!(
2824 "class Service:\r\n{}",
2825 header
2826 .split_inclusive('\n')
2827 .map(|line| format!("{indentation}{line}"))
2828 .collect::<String>()
2829 )
2830 };
2831 let parsed = parse(&source);
2832 let metadata = parsed
2833 .signature_metadata
2834 .values()
2835 .flatten()
2836 .find(|metadata| metadata.parameters().len() == 2)
2837 .expect("CRLF callable metadata");
2838 assert!(!metadata.label().contains('\r'));
2839 let labels = metadata
2840 .parameters()
2841 .iter()
2842 .map(|parameter| &metadata.label()[parameter.start_byte()..parameter.end_byte()])
2843 .collect::<Vec<_>>();
2844 assert_eq!(labels, ["first", "second"]);
2845 }
2846 }
2847
2848 #[test]
2849 #[brokk_bifrost_test_macros::hardwon(
2850 "#4455: multiline Python parameter offsets dropped a newline before an unindented label"
2851 )]
2852 fn multiline_parameter_offsets_follow_ast_ranges_through_header_normalization() {
2853 let source = concat!(
2854 "def manyargs(first: Annotated[int, 'second'] = second, second: int = 1,\r\n",
2855 "# comment between parameters\r\n",
2856 "third: int = second,\r\n",
2857 "/,\r\n",
2858 "*args: object,\r\n",
2859 "keyword: int = 2,\r\n",
2860 "**kwargs: object,\r\n",
2861 "):\r\n",
2862 " pass\r\n",
2863 "def keyword_only(\r\n",
2864 "positional,\r\n",
2865 "*,\r\n",
2866 "required,\r\n",
2867 "):\r\n",
2868 " pass\r\n",
2869 "class Service:\n",
2870 "\tdef tabbed(\n",
2871 "\t\tfirst,\n",
2872 "\t\tsecond,\n",
2873 "\t):\n",
2874 "\t\tpass\n",
2875 );
2876 let parsed = parse(source);
2877 let metadata = |name: &str| {
2878 parsed
2879 .signature_metadata
2880 .iter()
2881 .find(|(unit, _)| unit.short_name() == name)
2882 .unwrap_or_else(|| {
2883 let available = parsed
2884 .signature_metadata
2885 .keys()
2886 .map(|unit| unit.short_name())
2887 .collect::<Vec<_>>();
2888 panic!("callable metadata for {name}; available names: {available:?}");
2889 })
2890 .1
2891 .first()
2892 .expect("one signature")
2893 };
2894 let parameter_names = |name: &str| {
2895 let metadata = metadata(name);
2896 metadata
2897 .parameters()
2898 .iter()
2899 .map(|parameter| &metadata.label()[parameter.start_byte()..parameter.end_byte()])
2900 .collect::<Vec<_>>()
2901 };
2902
2903 let manyargs = metadata("manyargs");
2904 assert_eq!(
2905 parameter_names("manyargs"),
2906 ["first", "second", "third", "args", "keyword", "kwargs"]
2907 );
2908 assert!(
2909 manyargs
2910 .label()
2911 .contains("# comment between parameters\nthird: int")
2912 );
2913 assert!(manyargs.label().contains("\n/,\n*args: object"));
2914 assert!(!manyargs.label().contains('\r'));
2915 assert_eq!(parameter_names("keyword_only"), ["positional", "required"]);
2916 assert_eq!(parameter_names("Service.tabbed"), ["first", "second"]);
2917 }
2918
2919 #[test]
2920 fn empty_and_recovered_python_publish_canonical_structural_facts() {
2921 for source in [
2922 "",
2923 "class Broken(:\n pass\ndef valid(value):\n return value\n",
2924 "def broken(:\n pass\n",
2925 ] {
2926 let parsed = parse(source);
2927 let facts = parsed.source_facts.expect("complete producer publication");
2928 assert_eq!(facts.source_bytes, source.len());
2929 assert!(facts.python.is_some());
2930 assert_eq!(parsed.resolution_facts, Default::default());
2931 assert!(facts.native_site_occurrences.is_empty());
2932 assert!(facts.native_declaration_sources.is_empty());
2933 assert!(
2934 facts
2935 .occurrences
2936 .occurrences()
2937 .iter()
2938 .all(|occurrence| occurrence.range.end_byte <= source.len())
2939 );
2940 }
2941 }
2942
2943 #[test]
2944 fn unmounted_annotated_functions_keep_source_declarations_and_return_facts() {
2945 let source = "if outer:\n if inner:\n def hidden(value: int) -> Hidden:\n pass\n\ndef visible() -> Visible:\n pass\n";
2946 let parsed = parse(source);
2947 let facts = parsed
2948 .source_facts
2949 .as_ref()
2950 .expect("canonical Python facts");
2951 let python = facts.python.as_ref().expect("Python source facts");
2952 assert_eq!(python.callable_returns.len(), 2);
2953
2954 let fact_named = |name: &str| {
2955 python
2956 .callable_returns
2957 .iter()
2958 .find(|fact| {
2959 let declaration = facts.occurrences.declaration(fact.declaration);
2960 let Some(name_occurrence) = declaration.name else {
2961 return false;
2962 };
2963 let range = facts.occurrences.occurrence(name_occurrence).range;
2964 &source[range.start_byte..range.end_byte] == name
2965 })
2966 .expect("callable return fact")
2967 };
2968 let hidden = fact_named("hidden");
2969 let hidden_declaration = facts.occurrences.declaration(hidden.declaration);
2970 let hidden_range = facts
2971 .occurrences
2972 .occurrence(hidden_declaration.occurrence)
2973 .range;
2974 assert_eq!(
2975 &source[hidden_range.start_byte..hidden_range.start_byte + 3],
2976 "def"
2977 );
2978 let hidden_annotation = hidden.return_annotation.expect("hidden annotation");
2979 let hidden_annotation_range = facts.occurrences.occurrence(hidden_annotation).range;
2980 assert_eq!(
2981 &source[hidden_annotation_range.start_byte..hidden_annotation_range.end_byte],
2982 "Hidden"
2983 );
2984 assert!(
2985 parsed
2986 .source_declaration_units
2987 .iter()
2988 .all(|(_, unit)| unit.short_name() != "hidden")
2989 );
2990
2991 let visible = fact_named("visible");
2992 assert!(visible.return_annotation.is_some());
2993 assert!(visible.runtime_type.is_some());
2994 assert!(
2995 parsed
2996 .source_declaration_units
2997 .iter()
2998 .any(|(_, unit)| unit.short_name() == "visible")
2999 );
3000 let visible_metadata = parsed
3001 .signature_metadata
3002 .iter()
3003 .find(|(unit, _)| unit.short_name() == "visible")
3004 .and_then(|(_, metadata)| metadata.first())
3005 .expect("visible signature metadata");
3006 assert_eq!(visible_metadata.return_type_text(), Some("Visible"));
3007 assert_eq!(
3008 visible_metadata.return_type_identity(),
3009 visible.runtime_type.as_ref()
3010 );
3011 }
3012
3013 #[test]
3014 fn return_fact_preserves_unsupported_annotation_without_runtime_identity() {
3015 let source = "def unsupported() -> Left | Right:\n pass\n\ndef plain():\n pass\n";
3016 let parsed = parse(source);
3017 let facts = parsed
3018 .source_facts
3019 .as_ref()
3020 .expect("canonical Python facts");
3021 let python = facts.python.as_ref().expect("Python source facts");
3022 let by_name = |name: &str| {
3023 python
3024 .callable_returns
3025 .iter()
3026 .find(|fact| {
3027 let declaration = facts.occurrences.declaration(fact.declaration);
3028 let Some(name_occurrence) = declaration.name else {
3029 return false;
3030 };
3031 let range = facts.occurrences.occurrence(name_occurrence).range;
3032 &source[range.start_byte..range.end_byte] == name
3033 })
3034 .expect("callable return fact")
3035 };
3036 let unsupported = by_name("unsupported");
3037 assert!(unsupported.return_annotation.is_some());
3038 assert!(unsupported.runtime_type.is_none());
3039 let plain = by_name("plain");
3040 assert!(plain.return_annotation.is_none());
3041 assert!(plain.runtime_type.is_none());
3042 }
3043
3044 #[test]
3045 #[brokk_bifrost_test_macros::hardwon(
3046 "45e71db39f13: generic Python annotations mistook a list element for the returned class owner"
3047 )]
3048 fn return_annotation_references_preserve_runtime_owners() {
3049 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";
3050 let parsed = parse(source);
3051 let facts = parsed
3052 .source_facts
3053 .as_ref()
3054 .expect("canonical Python facts");
3055 let python = facts.python.as_ref().expect("Python source facts");
3056 let fact_named = |name: &str| {
3057 python
3058 .callable_returns
3059 .iter()
3060 .find(|fact| {
3061 let declaration = facts.occurrences.declaration(fact.declaration);
3062 let Some(name_occurrence) = declaration.name else {
3063 return false;
3064 };
3065 let range = facts.occurrences.occurrence(name_occurrence).range;
3066 &source[range.start_byte..range.end_byte] == name
3067 })
3068 .expect("callable return fact")
3069 };
3070 let generic = fact_named("generic");
3071 assert_eq!(
3072 generic
3073 .annotation_references
3074 .iter()
3075 .map(|reference| &reference.name)
3076 .collect::<Vec<_>>(),
3077 vec![&PythonAnnotationReferenceName::Lexical("list".to_string())]
3078 );
3079
3080 let nested = fact_named("nested");
3082 assert_eq!(nested.annotation_references.len(), 1);
3083 assert!(matches!(
3084 &nested.annotation_references[0].name,
3085 PythonAnnotationReferenceName::Lexical(name) if name == "list"
3086 ));
3087
3088 let direct = fact_named("direct");
3089 assert_eq!(direct.annotation_references.len(), 3);
3090 assert_eq!(direct.annotation_references[0].subtree_end, 3);
3091 assert_eq!(direct.annotation_references[0].lookup_depth, 1);
3092 assert!(matches!(
3093 &direct.annotation_references[0].name,
3094 PythonAnnotationReferenceName::Qualified(path)
3095 if path == &vec!["pkg".to_string(), "User".to_string()]
3096 ));
3097
3098 let quoted = fact_named("quoted");
3099 assert_eq!(quoted.annotation_references.len(), 1);
3100 assert!(matches!(
3101 "ed.annotation_references[0].name,
3102 PythonAnnotationReferenceName::Lexical(name) if name == "pkg.User"
3103 ));
3104 assert_eq!(quoted.annotation_references[0].lookup_depth, 2);
3105
3106 let unsupported = fact_named("unsupported");
3107 assert!(
3108 unsupported
3109 .annotation_references
3110 .iter()
3111 .all(|reference| !matches!(
3112 &reference.name,
3113 PythonAnnotationReferenceName::Qualified(_)
3114 ))
3115 );
3116 }
3117}
3118
3119fn python_chained_assignment_targets<'tree>(assignment: Node<'tree>) -> Vec<Node<'tree>> {
3126 super::imports::python_assignment_chain(assignment).0
3127}
3128
3129#[cfg(test)]
3130mod supertype_tests {
3131 use super::extract_python_supertypes;
3132 use tree_sitter::Node;
3133
3134 fn class_node<'tree>(tree: &'tree tree_sitter::Tree, source: &str) -> Node<'tree> {
3135 let mut cursor = tree.root_node().walk();
3136 tree.root_node()
3137 .named_children(&mut cursor)
3138 .find(|node| node.kind() == "class_definition")
3139 .unwrap_or_else(|| panic!("source declares a class: {source}"))
3140 }
3141
3142 fn parse(source: &str) -> tree_sitter::Tree {
3143 super::parse_python_tree(source).expect("the source parses")
3144 }
3145
3146 #[test]
3147 #[brokk_bifrost_test_macros::hardwon(
3148 "50c1574b2070: unsupported Python positional bases were dropped and created false complete absence"
3149 )]
3150 fn every_positional_base_contributes_a_spelling() {
3151 for (source, expected) in [
3152 ("class A(Base): pass\n", vec!["Base"]),
3153 ("class A(pkg.Base): pass\n", vec!["pkg.Base"]),
3154 ("class A(Base[int]): pass\n", vec!["Base"]),
3157 ("class A(pkg.Base[str, int]): pass\n", vec!["pkg.Base"]),
3158 (
3159 "class A(Mapping[str, Any], Base): pass\n",
3160 vec!["Mapping", "Base"],
3161 ),
3162 ("class A(Base, metaclass=Meta): pass\n", vec!["Base"]),
3164 ("class A(metaclass=Meta): pass\n", Vec::new()),
3165 (
3168 "class A(namedtuple(\"P\", \"x\")): pass\n",
3169 vec!["namedtuple(\"P\", \"x\")"],
3170 ),
3171 ("class A(*bases): pass\n", vec!["*bases"]),
3172 ("class A: pass\n", Vec::new()),
3173 ] {
3174 let tree = parse(source);
3175 let node = class_node(&tree, source);
3176 assert_eq!(
3177 extract_python_supertypes(node, source),
3178 expected,
3179 "supertypes of {source}"
3180 );
3181 }
3182 }
3183}
3184
3185#[cfg(test)]
3186mod callable_modifier_tests {
3187 use super::*;
3188
3189 #[test]
3212 #[brokk_bifrost_test_macros::hardwon(
3213 "#3451: Python callable receiver modifiers were missing, leaving workspace procedures unkeyable"
3214 )]
3215 fn callable_metadata_records_python_receiver_contracts_structurally() {
3216 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";
3217 let file = ProjectFile::new(std::env::temp_dir(), "widget.py");
3218 let tree = parse_python_tree(source).expect("parse the Python fixture");
3219 let parsed = parse_python_file(
3220 &brokk_bifrost_core::analyzer::project::TestProject::new(
3221 file.root(),
3222 brokk_bifrost_core::analyzer::Language::Python,
3223 ),
3224 &file,
3225 source,
3226 &tree,
3227 );
3228
3229 let entry = |fq_name: &str| {
3230 parsed
3231 .signature_metadata
3232 .iter()
3233 .find(|(unit, _)| unit.fq_name() == fq_name)
3234 .unwrap_or_else(|| {
3235 panic!(
3236 "missing Python declaration {fq_name}; recorded {:?}",
3237 parsed
3238 .signature_metadata
3239 .keys()
3240 .map(CodeUnit::fq_name)
3241 .collect::<Vec<_>>()
3242 )
3243 })
3244 };
3245 let modifiers = |fq_name: &str| {
3246 let (_, entries) = entry(fq_name);
3247 let metadata = entries
3248 .first()
3249 .unwrap_or_else(|| panic!("{fq_name} carries no signature metadata"));
3250 assert!(
3251 metadata.callable_modifiers_recorded(),
3252 "{fq_name} must record that the walk read its declaration shape"
3253 );
3254 (
3255 metadata.callable_is_static(),
3256 metadata.callable_is_constructor(),
3257 metadata.parameters().len(),
3258 )
3259 };
3260
3261 assert_eq!(modifiers("widget.free"), (false, false, 1));
3262 assert_eq!(
3263 modifiers("widget.Widget.__init__"),
3264 (false, false, 2),
3265 "`__init__` is an ordinary instance method, exactly as Ruby's `initialize`"
3266 );
3267 assert_eq!(modifiers("widget.Widget.render"), (false, false, 2));
3268 assert_eq!(
3269 modifiers("widget.Widget.build"),
3270 (true, false, 1),
3271 "`@staticmethod` binds no instance receiver"
3272 );
3273 assert_eq!(
3274 modifiers("widget.Widget.measure"),
3275 (true, false, 2),
3276 "`@classmethod` receives the class, not an instance"
3277 );
3278 assert_eq!(
3279 modifiers("widget.fetch_all"),
3280 (false, false, 1),
3281 "an async def is the same function_definition node"
3282 );
3283 assert_eq!(modifiers("widget.outer"), (false, false, 1));
3284
3285 let (property_unit, property_entries) = entry("widget.Widget.label");
3286 assert_eq!(
3287 property_unit.kind(),
3288 CodeUnitType::Field,
3289 "`@property` is read as an attribute, so it mints a Field and no key"
3290 );
3291 assert!(!property_unit.is_callable());
3292 assert!(
3293 property_entries
3294 .first()
3295 .expect("the property still carries signature metadata")
3296 .callable_modifiers_recorded(),
3297 "the shared metadata path records modifiers for every declaration it mints"
3298 );
3299
3300 let lambda_unit = parsed
3305 .declarations()
3306 .iter()
3307 .find(|unit| unit.fq_name() == "widget.assigned")
3308 .expect("the module walk still records the name-bound lambda's assignment");
3309 assert_eq!(
3310 lambda_unit.kind(),
3311 CodeUnitType::Field,
3312 "a name-bound lambda is recorded as a field, not as a callable declaration"
3313 );
3314 assert!(
3315 !parsed.signature_metadata.contains_key(lambda_unit),
3316 "a field assignment carries signature text but no callable signature metadata"
3317 );
3318 assert!(
3319 parsed
3320 .declarations()
3321 .iter()
3322 .all(|unit| unit.terminal_name() != "inner"),
3323 "a function nested in another function is a local binding, not a declaration"
3324 );
3325 }
3326}