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
16fn py_segment(text: &str, kind: SegmentKind) -> SegmentId {
18 segment_interner().intern(text, kind)
19}
20
21pub 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
91fn 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 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 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 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
641pub 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
766pub 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 "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}