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