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