Skip to main content

brokk_bifrost_python/
declarations.rs

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