Skip to main content

brokk_bifrost_python/
declarations.rs

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