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