Skip to main content

brokk_bifrost_python/
declarations.rs

1use crate::imports::python_import_infos_from_node;
2use crate::syntax::{PythonOverloadDecoratorBindings, expression_name_node};
3use brokk_bifrost_core::analyzer::fq_name::{FqName, SegmentId, SegmentKind, segment_interner};
4use brokk_bifrost_core::analyzer::model::{
5    CodeUnitType, DispatchExtensibility, ParameterMetadata, SignatureMetadata,
6};
7use brokk_bifrost_core::analyzer::parsed_file::ParsedFile;
8use brokk_bifrost_core::analyzer::tree_walk::{WalkControl, walk_named_tree_preorder};
9use brokk_bifrost_core::analyzer::{CodeUnit, ProjectFile};
10use brokk_bifrost_core::hash::HashSet;
11use brokk_bifrost_core::path_normalization::NormalizePath;
12use brokk_bifrost_core::text_utils::{compute_line_starts, find_line_index_for_offset};
13use std::path::{Path, PathBuf};
14use tree_sitter::{Node, Parser, Tree};
15
16/// Intern one qualified-name segment in the process-global interner.
17fn py_segment(text: &str, kind: SegmentKind) -> SegmentId {
18    segment_interner().intern(text, kind)
19}
20
21/// Build the structured module-path prefix for a Python declaration.
22///
23/// Ordinary modules render as a dotted path such as `mypkg.subpkg.mymodule`,
24/// with each original path component represented by one
25/// [`SegmentKind::Package`] segment. Hidden directories such as `.agent` and
26/// `.github` are also legal components in the analyzer's path-derived Python
27/// convention, but their leading dot is ambiguous after a rendered name has
28/// been joined.
29///
30/// Build the structured name from the file path's original components so
31/// hidden-directory segments stay intact in cold extraction, synthesized module
32/// units, and persisted reconstruction.
33pub fn python_module_fq(file: &ProjectFile) -> FqName {
34    python_module_fq_from_components(&python_module_components(file))
35}
36
37fn python_module_fq_from_components(components: &[String]) -> FqName {
38    let mut fq = FqName::new();
39    for component in components {
40        fq.push(py_segment(component, SegmentKind::Package));
41    }
42    fq
43}
44
45fn python_module_components(file: &ProjectFile) -> Vec<String> {
46    let mut components = python_package_components_for_file(file);
47    let module_name = file
48        .rel_path()
49        .file_stem()
50        .and_then(|stem| stem.to_str())
51        .unwrap_or_default();
52    if module_name != "__init__" || components.is_empty() {
53        components.push(module_name.to_string());
54    }
55    components
56}
57
58fn python_package_components_for_file(file: &ProjectFile) -> Vec<String> {
59    let Some(parent_rel) = file.rel_path().parent() else {
60        return Vec::new();
61    };
62    if parent_rel.as_os_str().is_empty() {
63        return Vec::new();
64    }
65
66    if let Some(import_root_rel) = python_configured_import_root(file, parent_rel)
67        && let Ok(relative_package) = parent_rel.strip_prefix(import_root_rel)
68    {
69        return path_components(relative_package);
70    }
71
72    let mut effective_package_root_rel: Option<&Path> = None;
73    let mut current_rel = Some(parent_rel);
74    while let Some(path) = current_rel {
75        if file.root().join(path).join("__init__.py").exists() {
76            effective_package_root_rel = Some(path);
77        }
78        current_rel = path.parent();
79    }
80
81    let relative_package = match effective_package_root_rel {
82        Some(package_root_rel) => package_root_rel
83            .parent()
84            .and_then(|import_root_rel| parent_rel.strip_prefix(import_root_rel).ok())
85            .unwrap_or(parent_rel),
86        None => parent_rel,
87    };
88    path_components(relative_package)
89}
90
91/// Find the nearest setuptools import root that contains this source file.
92///
93/// The manifest is parsed as TOML. An unrelated or malformed `pyproject.toml`
94/// does not change the existing `__init__.py` package-root convention.
95fn python_configured_import_root(file: &ProjectFile, parent_rel: &Path) -> Option<PathBuf> {
96    let mut manifest_dir_rel = Some(parent_rel);
97    while let Some(directory) = manifest_dir_rel {
98        let manifest = file.root().join(directory).join("pyproject.toml");
99        if let Ok(source) = std::fs::read_to_string(manifest)
100            && let Ok(document) = source.parse::<toml::Value>()
101            && let Some(where_entries) = document
102                .get("tool")
103                .and_then(|tool| tool.get("setuptools"))
104                .and_then(|setuptools| setuptools.get("packages"))
105                .and_then(|packages| packages.get("find"))
106                .and_then(|find| find.get("where"))
107                .and_then(toml::Value::as_array)
108        {
109            let mut roots = where_entries
110                .iter()
111                .filter_map(toml::Value::as_str)
112                .map(|entry| file.root().join(directory).join(entry).normalize())
113                .filter_map(|root| root.strip_prefix(file.root()).ok().map(Path::to_path_buf))
114                .filter(|root| parent_rel.starts_with(root))
115                .collect::<Vec<_>>();
116            roots.sort_by_key(|root| root.components().count());
117            if let Some(root) = roots.pop() {
118                return Some(root);
119            }
120        }
121        manifest_dir_rel = directory.parent();
122    }
123    None
124}
125
126fn path_components(path: &Path) -> Vec<String> {
127    path.components()
128        .map(|component| component.as_os_str().to_string_lossy().to_string())
129        .filter(|component| !component.is_empty())
130        .collect()
131}
132
133pub fn python_is_decorated_function_boundary(node: Node<'_>) -> bool {
134    if node.kind() != "decorated_definition" {
135        return false;
136    }
137    let mut cursor = node.walk();
138    node.named_children(&mut cursor)
139        .any(|child| child.kind() == "function_definition")
140}
141
142#[derive(Clone)]
143pub struct Scope {
144    kind: ScopeKind,
145    path: String,
146    /// The structured qualified name matching `path` (M1 dual representation;
147    /// see `.agents/plans/fqname-interned-segments.md`). Tracked independent of
148    /// whether this scope level was actually `capture`d as a `CodeUnit`, so a
149    /// nested class/function that IS captured can always extend an ancestor's
150    /// `fq` even when an intermediate scope level (e.g. a non-captured nested
151    /// function) has no `code_unit` of its own to read `.fq()` from.
152    fq: FqName,
153    code_unit: Option<CodeUnit>,
154    method_receiver: Option<String>,
155}
156
157#[derive(Clone, Copy, PartialEq, Eq)]
158enum ScopeKind {
159    Class,
160    Function,
161}
162
163pub struct PythonVisitor<'a> {
164    pub file: &'a ProjectFile,
165    pub source: &'a str,
166    pub package_name: &'a str,
167    module_fq: &'a FqName,
168    pub parsed: &'a mut ParsedFile,
169    pub module: Option<CodeUnit>,
170    pub overload_decorators: &'a PythonOverloadDecoratorBindings,
171}
172
173struct PythonContainer<'tree> {
174    node: Node<'tree>,
175    scope: Vec<Scope>,
176    module_control_depth: usize,
177}
178
179enum PythonWork<'tree> {
180    Container(PythonContainer<'tree>),
181    Statement {
182        node: Node<'tree>,
183        scope: Vec<Scope>,
184        module_control_depth: usize,
185    },
186}
187
188impl<'a> PythonVisitor<'a> {
189    pub fn visit_container(
190        &mut self,
191        node: Node<'_>,
192        scope: &[Scope],
193        module_control_depth: usize,
194    ) {
195        let mut stack = vec![PythonWork::Container(PythonContainer {
196            node,
197            scope: scope.to_vec(),
198            module_control_depth,
199        })];
200        while let Some(work) = stack.pop() {
201            match work {
202                PythonWork::Container(container) => {
203                    let mut cursor = container.node.walk();
204                    let children = container
205                        .node
206                        .named_children(&mut cursor)
207                        .collect::<Vec<_>>();
208                    for child in children.into_iter().rev() {
209                        stack.push(PythonWork::Statement {
210                            node: child,
211                            scope: container.scope.clone(),
212                            module_control_depth: container.module_control_depth,
213                        });
214                    }
215                }
216                PythonWork::Statement {
217                    node,
218                    scope,
219                    module_control_depth,
220                } => self.visit_statement(node, &scope, module_control_depth, &mut stack),
221            }
222        }
223    }
224
225    fn visit_statement<'tree>(
226        &mut self,
227        node: Node<'tree>,
228        scope: &[Scope],
229        module_control_depth: usize,
230        stack: &mut Vec<PythonWork<'tree>>,
231    ) {
232        match node.kind() {
233            "decorated_definition" => {
234                if let Some(definition) = node.child_by_field_name("definition") {
235                    self.visit_definition(
236                        definition,
237                        Some(node),
238                        scope,
239                        module_control_depth,
240                        stack,
241                    );
242                }
243            }
244            "class_definition" | "function_definition" => {
245                self.visit_definition(node, None, scope, module_control_depth, stack)
246            }
247            "expression_statement" => {
248                self.visit_expression_statement(node, scope, module_control_depth)
249            }
250            "import_statement" | "import_from_statement" => self.visit_import_statement(node),
251            "if_statement" | "try_statement" | "with_statement" | "for_statement"
252            | "while_statement" => {
253                let next_depth = if scope.is_empty() {
254                    module_control_depth + 1
255                } else {
256                    module_control_depth
257                };
258                stack.push(PythonWork::Container(PythonContainer {
259                    node,
260                    scope: scope.to_vec(),
261                    module_control_depth: next_depth,
262                }));
263            }
264            "elif_clause" | "else_clause" | "except_clause" | "finally_clause" => {
265                stack.push(PythonWork::Container(PythonContainer {
266                    node,
267                    scope: scope.to_vec(),
268                    module_control_depth,
269                }));
270            }
271            "block" | "module" => stack.push(PythonWork::Container(PythonContainer {
272                node,
273                scope: scope.to_vec(),
274                module_control_depth,
275            })),
276            _ => {}
277        }
278    }
279
280    fn visit_definition<'tree>(
281        &mut self,
282        definition: Node<'tree>,
283        wrapper: Option<Node<'tree>>,
284        scope: &[Scope],
285        module_control_depth: usize,
286        stack: &mut Vec<PythonWork<'tree>>,
287    ) {
288        match definition.kind() {
289            "class_definition" => self.visit_class_definition(
290                definition,
291                wrapper.unwrap_or(definition),
292                scope,
293                module_control_depth,
294                stack,
295            ),
296            "function_definition" => self.visit_function_definition(
297                definition,
298                wrapper.unwrap_or(definition),
299                scope,
300                module_control_depth,
301                stack,
302            ),
303            _ => {}
304        }
305    }
306
307    fn visit_class_definition<'tree>(
308        &mut self,
309        node: Node<'tree>,
310        range_node: Node<'tree>,
311        scope: &[Scope],
312        module_control_depth: usize,
313        stack: &mut Vec<PythonWork<'tree>>,
314    ) {
315        let Some(name_node) = node.child_by_field_name("name") else {
316            return;
317        };
318        let name = py_node_text(name_node, self.source).trim();
319        if name.is_empty() {
320            return;
321        }
322
323        let capture = !scope.is_empty() || module_control_depth <= 1;
324
325        let short_name = scope
326            .last()
327            .map(|parent| format!("{}${name}", parent.path))
328            .unwrap_or_else(|| name.to_string());
329        // A nested class (any parent scope, Class or Function) is always joined
330        // with a literal `$` in the legacy convention above, which is exactly
331        // what `SegmentKind::Nested` renders regardless of the preceding
332        // segment's kind; a top-level class has no parent and is a plain `Type`
333        // hanging off the module-path `Package` chain.
334        let fq = match scope.last() {
335            Some(parent) => parent
336                .fq
337                .clone()
338                .with_pushed(py_segment(name, SegmentKind::Nested)),
339            None => self
340                .module_fq
341                .clone()
342                .with_pushed(py_segment(name, SegmentKind::Type)),
343        };
344        let code_unit = CodeUnit::new_fq(
345            self.file.clone(),
346            CodeUnitType::Class,
347            self.package_name.to_string(),
348            short_name.clone(),
349            fq.clone(),
350        );
351        if capture {
352            self.parsed
353                .replace_code_unit(code_unit.clone(), range_node, self.source, None, None);
354            self.parsed.add_signature(
355                code_unit.clone(),
356                python_class_signature(range_node, self.source),
357            );
358            if let Some(module) = &self.module
359                && scope.is_empty()
360            {
361                self.parsed.add_child(module.clone(), code_unit.clone());
362            }
363            if let Some(parent) = scope.last()
364                && let Some(parent_cu) = &parent.code_unit
365            {
366                self.parsed.add_child(parent_cu.clone(), code_unit.clone());
367            }
368            self.parsed.set_raw_supertypes(
369                code_unit.clone(),
370                extract_python_supertypes(node, self.source),
371            );
372        }
373
374        let mut next_scope = scope.to_vec();
375        if capture {
376            next_scope.push(Scope {
377                kind: ScopeKind::Class,
378                path: short_name,
379                fq,
380                code_unit: Some(code_unit),
381                method_receiver: None,
382            });
383        }
384        if let Some(body) = node.child_by_field_name("body") {
385            stack.push(PythonWork::Container(PythonContainer {
386                node: body,
387                scope: next_scope,
388                module_control_depth,
389            }));
390        }
391    }
392
393    fn visit_function_definition<'tree>(
394        &mut self,
395        node: Node<'tree>,
396        range_node: Node<'tree>,
397        scope: &[Scope],
398        module_control_depth: usize,
399        stack: &mut Vec<PythonWork<'tree>>,
400    ) {
401        let Some(name_node) = node.child_by_field_name("name") else {
402            return;
403        };
404        let name = py_node_text(name_node, self.source).trim();
405        if name.is_empty() {
406            return;
407        }
408
409        let capture = !python_is_property_mutator(range_node, self.source)
410            && ((scope.is_empty() && module_control_depth <= 1)
411                || scope
412                    .last()
413                    .is_some_and(|parent| parent.kind == ScopeKind::Class));
414        let short_name = if let Some(parent) = scope.last() {
415            match parent.kind {
416                ScopeKind::Class => format!("{}.{}", parent.path, name),
417                ScopeKind::Function => format!("{}${name}", parent.path),
418            }
419        } else {
420            name.to_string()
421        };
422        // Mirrors `short_name` above segment-for-segment: a method owned
423        // directly by a class joins with `.` (`Member`), while a function
424        // nested under another function is a local/closure and joins with the
425        // literal `$` that `SegmentKind::Nested` renders.
426        let fq = if let Some(parent) = scope.last() {
427            match parent.kind {
428                ScopeKind::Class => parent
429                    .fq
430                    .clone()
431                    .with_pushed(py_segment(name, SegmentKind::Member)),
432                ScopeKind::Function => parent
433                    .fq
434                    .clone()
435                    .with_pushed(py_segment(name, SegmentKind::Nested)),
436            }
437        } else {
438            self.module_fq
439                .clone()
440                .with_pushed(py_segment(name, SegmentKind::Member))
441        };
442
443        if capture {
444            let code_unit_type = if python_function_has_decorator(node, self.source, "property") {
445                CodeUnitType::Field
446            } else {
447                CodeUnitType::Function
448            };
449            let signature = node
450                .child_by_field_name("parameters")
451                .map(|parameters| py_node_text(parameters, self.source).trim().to_string());
452            let code_unit = CodeUnit::with_signature_and_fq(
453                self.file.clone(),
454                code_unit_type,
455                self.package_name.to_string(),
456                short_name.clone(),
457                signature,
458                false,
459                fq.clone(),
460            );
461            self.parsed
462                .replace_code_unit(code_unit.clone(), range_node, self.source, None, None);
463            let signature = python_function_signature(range_node, self.source);
464            self.parsed.add_signature_with_metadata(
465                code_unit.clone(),
466                python_signature_metadata(signature, node, self.source).with_declaration_only(
467                    self.overload_decorators
468                        .decorates_as_overload(node, self.source),
469                ),
470            );
471            if let Some(module) = &self.module
472                && scope.is_empty()
473            {
474                self.parsed.add_child(module.clone(), code_unit.clone());
475            }
476            if let Some(parent) = scope.last()
477                && parent.kind == ScopeKind::Class
478                && let Some(parent_cu) = &parent.code_unit
479            {
480                self.parsed.add_child(parent_cu.clone(), code_unit.clone());
481            }
482            let scope_code_unit = Some(code_unit);
483            let mut next_scope = scope.to_vec();
484            next_scope.push(Scope {
485                kind: ScopeKind::Function,
486                path: short_name,
487                fq,
488                code_unit: scope_code_unit,
489                method_receiver: scope
490                    .last()
491                    .is_some_and(|parent| parent.kind == ScopeKind::Class)
492                    .then(|| python_instance_method_receiver_name(node, self.source))
493                    .flatten(),
494            });
495            if let Some(body) = node.child_by_field_name("body") {
496                stack.push(PythonWork::Container(PythonContainer {
497                    node: body,
498                    scope: next_scope,
499                    module_control_depth,
500                }));
501            }
502            return;
503        }
504
505        let mut next_scope = scope.to_vec();
506        next_scope.push(Scope {
507            kind: ScopeKind::Function,
508            path: short_name,
509            fq,
510            code_unit: None,
511            method_receiver: None,
512        });
513        if let Some(body) = node.child_by_field_name("body") {
514            stack.push(PythonWork::Container(PythonContainer {
515                node: body,
516                scope: next_scope,
517                module_control_depth,
518            }));
519        }
520    }
521
522    fn visit_expression_statement(
523        &mut self,
524        node: Node<'_>,
525        scope: &[Scope],
526        module_control_depth: usize,
527    ) {
528        let Some(assignment) = node.named_child(0) else {
529            return;
530        };
531        if assignment.kind() != "assignment" {
532            return;
533        }
534        let Some(left) = assignment.child_by_field_name("left") else {
535            return;
536        };
537        self.visit_instance_attribute_assignment(left, scope);
538        let names = collect_assigned_names(left, self.source);
539        for name in names {
540            let (short_name, fq) = if let Some(parent) = scope.last() {
541                if parent.kind != ScopeKind::Class {
542                    continue;
543                }
544                (
545                    format!("{}.{}", parent.path, name),
546                    parent
547                        .fq
548                        .clone()
549                        .with_pushed(py_segment(&name, SegmentKind::Member)),
550                )
551            } else if module_control_depth <= 1 {
552                (
553                    name.clone(),
554                    self.module_fq
555                        .clone()
556                        .with_pushed(py_segment(&name, SegmentKind::Member)),
557                )
558            } else {
559                continue;
560            };
561            let code_unit = CodeUnit::new_fq(
562                self.file.clone(),
563                CodeUnitType::Field,
564                self.package_name.to_string(),
565                short_name,
566                fq,
567            );
568            self.parsed
569                .replace_code_unit(code_unit.clone(), node, self.source, None, None);
570            self.parsed.add_signature(
571                code_unit.clone(),
572                py_node_text(node, self.source).trim().to_string(),
573            );
574            if let Some(module) = &self.module
575                && scope.is_empty()
576            {
577                self.parsed.add_child(module.clone(), code_unit.clone());
578            }
579            if let Some(parent) = scope.last()
580                && parent.kind == ScopeKind::Class
581                && let Some(parent_cu) = &parent.code_unit
582            {
583                self.parsed.add_child(parent_cu.clone(), code_unit);
584            }
585        }
586    }
587
588    fn visit_instance_attribute_assignment(&mut self, left: Node<'_>, scope: &[Scope]) {
589        let Some(function) = scope
590            .last()
591            .filter(|scope| scope.kind == ScopeKind::Function)
592        else {
593            return;
594        };
595        let Some(receiver) = function.method_receiver.as_deref() else {
596            return;
597        };
598        let Some(parent) = scope
599            .get(scope.len().saturating_sub(2))
600            .filter(|scope| scope.kind == ScopeKind::Class)
601        else {
602            return;
603        };
604        let Some(parent_cu) = parent.code_unit.clone() else {
605            return;
606        };
607        for (name, node) in collect_self_assigned_attributes(left, self.source, receiver) {
608            let code_unit = CodeUnit::new_fq(
609                self.file.clone(),
610                CodeUnitType::Field,
611                self.package_name.to_string(),
612                format!("{}.{}", parent.path, name),
613                parent
614                    .fq
615                    .clone()
616                    .with_pushed(py_segment(&name, SegmentKind::Member)),
617            );
618            if !self.parsed.contains_declaration(&code_unit) {
619                self.parsed.replace_code_unit(
620                    code_unit.clone(),
621                    node,
622                    self.source,
623                    Some(parent_cu.clone()),
624                    Some(parent_cu.clone()),
625                );
626            }
627            self.parsed.add_signature(
628                code_unit.clone(),
629                py_node_text(left, self.source).trim().to_string(),
630            );
631        }
632    }
633
634    fn visit_import_statement(&mut self, node: Node<'_>) {
635        for info in python_import_infos_from_node(node, self.source) {
636            self.parsed.imports.push(info);
637        }
638    }
639}
640
641/// Build the [`ParsedFile`] for one Python source file: module unit, type
642/// identifiers, and the declaration walk. `analyzer/python/adapter.rs`'s
643/// `LanguageAdapter::parse_file` is the only caller.
644pub fn parse_python_file(file: &ProjectFile, source: &str, tree: &Tree) -> ParsedFile {
645    let module_components = python_module_components(file);
646    let module_name = module_components.join(".");
647    let module_fq = python_module_fq_from_components(&module_components);
648    let mut parsed = ParsedFile::new(module_name.clone());
649    let root = tree.root_node();
650
651    collect_python_identifiers(root, source, &mut parsed.type_identifiers);
652
653    let module_code_unit = module_code_unit_from_fq(file, &module_components, module_fq.clone());
654    if let Some(module) = module_code_unit.clone() {
655        parsed.add_code_unit(module, root, source, None, None);
656    }
657
658    let overload_decorators = PythonOverloadDecoratorBindings::collect(root, source);
659    let mut visitor = PythonVisitor {
660        file,
661        source,
662        package_name: &module_name,
663        module_fq: &module_fq,
664        parsed: &mut parsed,
665        module: module_code_unit,
666        overload_decorators: &overload_decorators,
667    };
668    visitor.visit_container(root, &[], 0);
669
670    parsed
671}
672
673pub fn py_node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
674    brokk_bifrost_core::analyzer::common::node_source_text(node, source)
675}
676
677pub fn python_module_name(file: &ProjectFile) -> String {
678    python_module_components(file).join(".")
679}
680
681pub fn module_code_unit(file: &ProjectFile, module_fq: &str) -> Option<CodeUnit> {
682    if module_fq.is_empty() {
683        return None;
684    }
685    let components = python_module_components(file);
686    debug_assert_eq!(
687        module_fq,
688        components.join("."),
689        "module_code_unit must be built from the file's path-derived Python module name"
690    );
691    let structured_fq = python_module_fq_from_components(&components);
692    module_code_unit_from_fq(file, &components, structured_fq)
693}
694
695fn module_code_unit_from_fq(
696    file: &ProjectFile,
697    components: &[String],
698    structured_fq: FqName,
699) -> Option<CodeUnit> {
700    let (short_name, package_components) = components.split_last()?;
701    let package_name = package_components.join(".");
702    Some(CodeUnit::new_fq(
703        file.clone(),
704        CodeUnitType::Module,
705        package_name,
706        short_name.clone(),
707        structured_fq,
708    ))
709}
710
711fn python_class_signature(node: Node<'_>, source: &str) -> String {
712    python_header_with_decorators(node, source)
713}
714
715fn python_function_signature(node: Node<'_>, source: &str) -> String {
716    let header = python_header_with_decorators(node, source);
717    if let Some((head, tail)) = header.rsplit_once('\n') {
718        format!("{head}\n{tail} ...")
719    } else {
720        format!("{header} ...")
721    }
722}
723
724fn python_signature_metadata(signature: String, node: Node<'_>, source: &str) -> SignatureMetadata {
725    let Some(parameters_node) = node.child_by_field_name("parameters") else {
726        return SignatureMetadata::new(signature, Vec::new())
727            .with_dispatch_extensibility(DispatchExtensibility::Open);
728    };
729    let parameter_text = py_node_text(parameters_node, source).trim();
730    let Some(parameters_start) = signature.find(parameter_text) else {
731        return SignatureMetadata::new(signature, Vec::new())
732            .with_dispatch_extensibility(DispatchExtensibility::Open);
733    };
734    let parameters_end = parameters_start + parameter_text.len();
735    let mut search_start = parameters_start;
736    let parameters = python_parameter_label_nodes(parameters_node)
737        .into_iter()
738        .filter_map(|label_node| {
739            let label = py_node_text(label_node, source).trim();
740            if label.is_empty() || search_start > parameters_end {
741                return None;
742            }
743            let haystack = signature.get(search_start..parameters_end)?;
744            let relative_start = haystack.find(label)?;
745            let start_byte = search_start + relative_start;
746            let end_byte = start_byte + label.len();
747            search_start = end_byte;
748            Some(ParameterMetadata::new(label, start_byte, end_byte))
749        })
750        .collect();
751    SignatureMetadata::new(signature, parameters)
752        .with_dispatch_extensibility(DispatchExtensibility::Open)
753}
754
755fn python_parameter_label_nodes(parameters_node: Node<'_>) -> Vec<Node<'_>> {
756    let mut labels = Vec::new();
757    let mut cursor = parameters_node.walk();
758    for child in parameters_node.named_children(&mut cursor) {
759        if let Some(label_node) = python_parameter_label_node(child) {
760            labels.push(label_node);
761        }
762    }
763    labels
764}
765
766/// The identifier node that names one parameter's binding.
767///
768/// The grammar gives `default_parameter` and `typed_default_parameter` a
769/// `name` field but gives `typed_parameter` and the two splat patterns none,
770/// so a caller that reads only the field loses the binding name of every
771/// annotated parameter. Every Python surface that names parameters reads them
772/// through this function.
773pub fn python_parameter_label_node(node: Node<'_>) -> Option<Node<'_>> {
774    match node.kind() {
775        "identifier" => Some(node),
776        "typed_parameter"
777        | "typed_default_parameter"
778        | "default_parameter"
779        | "list_splat_pattern"
780        | "dictionary_splat_pattern"
781        | "keyword_separator" => node.child_by_field_name("name").or_else(|| {
782            let mut cursor = node.walk();
783            node.named_children(&mut cursor)
784                .find_map(python_parameter_label_node)
785        }),
786        _ => None,
787    }
788}
789
790fn python_is_property_mutator(node: Node<'_>, source: &str) -> bool {
791    python_header_with_decorators(node, source)
792        .lines()
793        .map(str::trim)
794        .filter(|line| line.starts_with('@'))
795        .any(|decorator| decorator.ends_with(".setter") || decorator.ends_with(".deleter"))
796}
797
798pub fn python_expanded_comment_start(source: &str, start_byte: usize) -> usize {
799    let line_starts = compute_line_starts(source);
800    let line_index = find_line_index_for_offset(&line_starts, start_byte);
801
802    let mut comment_start = start_byte;
803    for line_idx in (0..line_index).rev() {
804        let line_start = line_starts[line_idx];
805        let line_end = line_starts
806            .get(line_idx + 1)
807            .copied()
808            .unwrap_or(source.len());
809        let line = &source[line_start..line_end];
810        let trimmed = line.trim_start();
811
812        if trimmed.trim().is_empty() {
813            continue;
814        }
815
816        if trimmed.starts_with('#') {
817            comment_start = line_start;
818            continue;
819        }
820
821        break;
822    }
823
824    comment_start
825}
826
827fn python_header_with_decorators(node: Node<'_>, source: &str) -> String {
828    let raw = py_node_text(node, source);
829    let lines: Vec<_> = raw
830        .lines()
831        .map(str::trim_end)
832        .filter(|line| !line.trim().is_empty())
833        .collect();
834    let mut relevant = Vec::new();
835    for line in lines {
836        let trimmed = line.trim_start();
837        if trimmed.starts_with('@')
838            || trimmed.starts_with("def ")
839            || trimmed.starts_with("async def ")
840            || trimmed.starts_with("class ")
841        {
842            relevant.push(trimmed.to_string());
843            if trimmed.starts_with("def ")
844                || trimmed.starts_with("async def ")
845                || trimmed.starts_with("class ")
846            {
847                break;
848            }
849        }
850    }
851    relevant.join("\n")
852}
853
854fn extract_python_supertypes(node: Node<'_>, source: &str) -> Vec<String> {
855    let Some(superclasses) = node.child_by_field_name("superclasses") else {
856        return Vec::new();
857    };
858    let mut result = Vec::new();
859    let mut cursor = superclasses.walk();
860    for child in superclasses.named_children(&mut cursor) {
861        match child.kind() {
862            "identifier" | "attribute" => {
863                let text = py_node_text(child, source).trim();
864                if !text.is_empty() {
865                    result.push(text.to_string());
866                }
867            }
868            _ => {}
869        }
870    }
871    result
872}
873
874fn collect_assigned_names(node: Node<'_>, source: &str) -> Vec<String> {
875    let mut names = Vec::new();
876    walk_named_tree_preorder(node, true, |node| {
877        match node.kind() {
878            // An attribute or subscript target (`foo.bar = …`, `foo[i] = …`)
879            // mutates an existing object; it declares neither the receiver nor
880            // the member as a name, so do not descend into it.
881            "attribute" | "subscript" => WalkControl::SkipChildren,
882            "identifier" => {
883                let text = py_node_text(node, source).trim();
884                if !text.is_empty() {
885                    names.push(text.to_string());
886                }
887                WalkControl::Continue
888            }
889            _ => WalkControl::Continue,
890        }
891    });
892    names
893}
894
895fn collect_self_assigned_attributes<'tree>(
896    node: Node<'tree>,
897    source: &str,
898    receiver_name: &str,
899) -> Vec<(String, Node<'tree>)> {
900    let mut attributes = Vec::new();
901    collect_direct_self_assigned_attributes(node, source, receiver_name, &mut attributes);
902    attributes
903}
904
905fn collect_direct_self_assigned_attributes<'tree>(
906    node: Node<'tree>,
907    source: &str,
908    receiver_name: &str,
909    attributes: &mut Vec<(String, Node<'tree>)>,
910) {
911    match node.kind() {
912        "attribute" => {
913            let Some(object) = node.child_by_field_name("object") else {
914                return;
915            };
916            if object.kind() != "identifier" || py_node_text(object, source).trim() != receiver_name
917            {
918                return;
919            }
920            let Some(attribute) = node.child_by_field_name("attribute") else {
921                return;
922            };
923            let name = py_node_text(attribute, source).trim();
924            if !name.is_empty() {
925                attributes.push((name.to_string(), attribute));
926            }
927        }
928        "pattern_list" | "tuple" | "list" | "parenthesized_expression" => {
929            let mut cursor = node.walk();
930            for child in node.named_children(&mut cursor) {
931                collect_direct_self_assigned_attributes(child, source, receiver_name, attributes);
932            }
933        }
934        _ => {}
935    }
936}
937
938fn python_instance_method_receiver_name(node: Node<'_>, source: &str) -> Option<String> {
939    if python_function_has_decorator(node, source, "staticmethod")
940        || python_function_has_decorator(node, source, "classmethod")
941    {
942        return None;
943    }
944    python_first_parameter_name(node, source)
945}
946
947fn python_function_has_decorator(node: Node<'_>, source: &str, decorator_name: &str) -> bool {
948    let Some(parent) = node.parent() else {
949        return false;
950    };
951    if parent.kind() != "decorated_definition" {
952        return false;
953    }
954    let mut cursor = parent.walk();
955    parent
956        .named_children(&mut cursor)
957        .filter(|child| child.kind() == "decorator")
958        .filter_map(|decorator| decorator.named_child(0))
959        .filter_map(expression_name_node)
960        .any(|name| py_node_text(name, source).trim() == decorator_name)
961}
962
963fn python_first_parameter_name(node: Node<'_>, source: &str) -> Option<String> {
964    let parameters = node.child_by_field_name("parameters")?;
965    let mut cursor = parameters.walk();
966    parameters
967        .named_children(&mut cursor)
968        .find_map(|child| python_parameter_name(child, source))
969}
970
971fn python_parameter_name(node: Node<'_>, source: &str) -> Option<String> {
972    match node.kind() {
973        "identifier" => Some(py_node_text(node, source).trim().to_string()),
974        "typed_parameter"
975        | "default_parameter"
976        | "list_splat_pattern"
977        | "dictionary_splat_pattern" => node
978            .child_by_field_name("name")
979            .or_else(|| {
980                let mut cursor = node.walk();
981                node.named_children(&mut cursor)
982                    .find(|child| child.kind() == "identifier")
983            })
984            .and_then(|name| python_parameter_name(name, source)),
985        _ => None,
986    }
987    .filter(|name| !name.is_empty())
988}
989
990pub fn collect_python_identifiers(node: Node<'_>, source: &str, identifiers: &mut HashSet<String>) {
991    walk_named_tree_preorder(node, true, |node| {
992        if node.kind() == "identifier" {
993            let text = py_node_text(node, source).trim();
994            if !text.is_empty() {
995                identifiers.insert(text.to_string());
996            }
997        }
998        WalkControl::Continue
999    });
1000}
1001
1002pub fn parse_python_tree(source: &str) -> Option<Tree> {
1003    let mut parser = Parser::new();
1004    parser
1005        .set_language(&tree_sitter_python::LANGUAGE.into())
1006        .expect("failed to load python parser");
1007    parser.parse(source, None)
1008}