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