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