1use brokk_bifrost_core::analyzer::Range;
2use brokk_bifrost_core::analyzer::common::node_source_text as node_text;
3use brokk_bifrost_core::cancellation::CancellationToken;
4use brokk_bifrost_core::hash::HashSet;
5use tree_sitter::{Node, Tree};
6
7pub(crate) fn python_conditional_expression_operands(node: Node<'_>) -> Option<[Node<'_>; 3]> {
15 assert_eq!(node.kind(), "conditional_expression");
16 let mut cursor = node.walk();
17 let operands: Vec<Node<'_>> = node
18 .named_children(&mut cursor)
19 .filter(|child| !child.is_extra())
20 .collect();
21 operands.try_into().ok()
22}
23
24pub(crate) fn python_parameter_name(node: Node<'_>, source: &str) -> Option<String> {
25 python_parameter_name_bounded(node, source, || true)
26 .expect("unbounded parameter name lookup completes")
27}
28
29pub(crate) fn python_parameter_name_bounded(
30 node: Node<'_>,
31 source: &str,
32 scope_step: impl FnMut() -> bool,
33) -> Option<Option<String>> {
34 python_parameter_node_bounded(node, scope_step).map(|node| {
35 node.and_then(|node| {
36 let name = node_text(node, source).trim();
37 (!name.is_empty()).then(|| name.to_owned())
38 })
39 })
40}
41
42#[cfg(any(test, feature = "test-support"))]
44pub(crate) fn python_parameter_node(node: Node<'_>) -> Option<Node<'_>> {
45 python_parameter_node_bounded(node, || true).expect("unbounded parameter token lookup")
46}
47
48fn python_parameter_node_bounded(
49 mut node: Node<'_>,
50 mut scope_step: impl FnMut() -> bool,
51) -> Option<Option<Node<'_>>> {
52 loop {
53 if !scope_step() {
54 return None;
55 }
56 match node.kind() {
57 "identifier" => return Some((!node.is_missing()).then_some(node)),
58 "typed_parameter"
59 | "typed_default_parameter"
60 | "default_parameter"
61 | "list_splat_pattern"
62 | "dictionary_splat_pattern" => {
63 if let Some(name) = node.child_by_field_name("name") {
64 node = name;
65 continue;
66 }
67 let mut selected = None;
68 let mut cursor = node.walk();
69 for child in node.named_children(&mut cursor) {
70 if !scope_step() {
71 return None;
72 }
73 if matches!(
74 child.kind(),
75 "identifier" | "list_splat_pattern" | "dictionary_splat_pattern"
76 ) {
77 selected = Some(child);
78 break;
79 }
80 }
81 let Some(name) = selected else {
82 return Some(None);
83 };
84 node = name;
85 }
86 _ => return Some(None),
87 }
88 }
89}
90
91pub fn leftmost_identifier(mut node: Node<'_>) -> Option<Node<'_>> {
92 loop {
93 match node.kind() {
94 "identifier" => return Some(node),
95 "attribute" => node = node.child_by_field_name("object")?,
96 _ => return None,
97 }
98 }
99}
100
101pub(crate) fn attribute_chain<'tree>(node: Node<'tree>) -> Option<(Node<'tree>, Vec<Node<'tree>>)> {
102 let mut attributes = Vec::new();
103 let mut current = node;
104 loop {
105 if current.kind() != "attribute" {
106 return None;
107 }
108 attributes.push(current.child_by_field_name("attribute")?);
109 current = current.child_by_field_name("object")?;
110 if current.kind() == "identifier" {
111 attributes.reverse();
112 return Some((current, attributes));
113 }
114 }
115}
116
117pub fn python_reference_is_deferred_function_body(node: Node<'_>) -> bool {
118 python_reference_is_deferred_function_body_bounded(node, || true)
119 .expect("unbounded deferred-context lookup completes")
120}
121
122pub fn python_reference_is_deferred_function_body_bounded(
124 node: Node<'_>,
125 mut scope_step: impl FnMut() -> bool,
126) -> Option<bool> {
127 let site_start = node.start_byte();
128 let site_end = node.end_byte();
129 let mut current = node;
130 while let Some(parent) = current.parent() {
131 if !scope_step() {
132 return None;
133 }
134 if matches!(parent.kind(), "function_definition" | "lambda")
135 && parent
136 .child_by_field_name("body")
137 .is_some_and(|body| body.start_byte() <= site_start && site_end <= body.end_byte())
138 {
139 return Some(true);
140 }
141 current = parent;
142 }
143 Some(false)
144}
145
146pub fn python_static_attribute_path<'tree>(node: Node<'tree>) -> Option<Vec<Node<'tree>>> {
152 python_static_value_path_with(node, || true, |_| None)
153 .expect("unbounded static path lookup completes")
154}
155
156pub fn python_static_attribute_path_bounded<'tree>(
159 node: Node<'tree>,
160 scope_step: impl FnMut() -> bool,
161) -> Option<Vec<Node<'tree>>> {
162 python_static_value_path_with(node, scope_step, |_| None).flatten()
163}
164
165pub fn python_static_value_path_bounded<'tree>(
168 node: Node<'tree>,
169 scope_step: impl FnMut() -> bool,
170) -> Option<Option<Vec<Node<'tree>>>> {
171 python_static_value_path_with(
172 node,
173 scope_step,
174 crate::structural::python_parenthesized_expression_child,
175 )
176}
177
178fn python_static_value_path_with<'tree>(
179 mut node: Node<'tree>,
180 mut scope_step: impl FnMut() -> bool,
181 mut parenthesized_child: impl FnMut(Node<'tree>) -> Option<Node<'tree>>,
182) -> Option<Option<Vec<Node<'tree>>>> {
183 let mut path = Vec::new();
184 loop {
185 if !scope_step() {
186 return None;
187 }
188 match node.kind() {
189 "identifier" => {
190 path.push(node);
191 break;
192 }
193 "attribute" => {
194 let Some(object) = push_static_attribute_segment(node, &mut path) else {
195 return Some(None);
196 };
197 node = object;
198 }
199 "parenthesized_expression" => {
200 for _ in brokk_bifrost_core::analyzer::tree_walk::named_children_iter(node) {
203 if !scope_step() {
204 return None;
205 }
206 }
207 let Some(child) = parenthesized_child(node) else {
208 return Some(None);
209 };
210 node = child;
211 }
212 _ => return Some(None),
213 }
214 }
215 for _ in 0..path.len() {
216 if !scope_step() {
217 return None;
218 }
219 }
220 path.reverse();
221 Some(Some(path))
222}
223
224pub fn python_static_type_path<'tree>(mut node: Node<'tree>) -> Option<Vec<Node<'tree>>> {
228 let mut path = Vec::new();
229 loop {
230 match node.kind() {
231 "identifier" => {
232 path.push(node);
233 break;
234 }
235 "type" | "generic_type" | "subscript" => node = node.named_child(0)?,
236 "attribute" => {
237 node = push_static_attribute_segment(node, &mut path)?;
238 }
239 "member_type" => {
240 let mut cursor = node.walk();
241 let mut children = node.named_children(&mut cursor);
242 let qualifier = children.next()?;
243 let member = children.next()?;
244 if member.kind() != "identifier" || children.next().is_some() {
245 return None;
246 }
247 path.push(member);
248 node = qualifier;
249 }
250 _ => return None,
251 }
252 }
253 path.reverse();
254 Some(path)
255}
256
257fn push_static_attribute_segment<'tree>(
258 node: Node<'tree>,
259 path: &mut Vec<Node<'tree>>,
260) -> Option<Node<'tree>> {
261 let attribute = node.child_by_field_name("attribute")?;
262 if attribute.kind() != "identifier" {
263 return None;
264 }
265 path.push(attribute);
266 node.child_by_field_name("object")
267}
268
269pub fn python_plain_string_literal<'source>(
275 node: Node<'_>,
276 source: &'source str,
277) -> Option<&'source str> {
278 if node.kind() != "string"
279 || node
280 .parent()
281 .is_some_and(|parent| parent.kind() == "concatenated_string")
282 {
283 return None;
284 }
285 let mut content = None;
286 let mut cursor = node.walk();
287 for child in node.named_children(&mut cursor) {
288 match child.kind() {
289 "string_start" | "string_end" => {
290 let delimiter = child.utf8_text(source.as_bytes()).ok()?;
291 if delimiter
292 .chars()
293 .any(|character| character != '"' && character != '\'')
294 {
295 return None;
296 }
297 }
298 "string_content" if child.named_child_count() == 0 && content.is_none() => {
299 content = Some(child);
300 }
301 _ => return None,
302 }
303 }
304 Some(content.map_or("", |child| {
305 child
306 .utf8_text(source.as_bytes())
307 .expect("a tree-sitter node range is valid UTF-8 source")
308 }))
309}
310
311#[derive(Debug, Default)]
312pub struct PythonOverloadDecoratorBindings {
313 direct: HashSet<String>,
314 namespaces: HashSet<String>,
315}
316
317#[derive(Debug)]
318pub(crate) enum PythonOverloadDecoratorName {
319 Direct(String),
320 Namespace(String),
321}
322
323impl PythonOverloadDecoratorBindings {
324 pub(crate) fn collect_import(
327 &mut self,
328 import: &brokk_bifrost_core::analyzer::model::ImportInfo,
329 ) {
330 use brokk_bifrost_core::analyzer::model::StructuredImportPathKind;
331 let Some(path) = &import.path else { return };
332 match (path.kind, path.segments.as_slice()) {
333 (Some(StructuredImportPathKind::Namespace), [module]) if is_typing_module(module) => {
334 self.namespaces
335 .insert(import.alias.as_ref().unwrap_or(module).clone());
336 }
337 (Some(StructuredImportPathKind::ImportFrom), [module, name])
338 if is_typing_module(module) && name == "overload" =>
339 {
340 self.direct
341 .insert(import.alias.as_ref().unwrap_or(name).clone());
342 }
343 _ => {}
344 }
345 }
346
347 pub(crate) fn overload_decorator_names(
352 function: Node<'_>,
353 source: &str,
354 ) -> Vec<PythonOverloadDecoratorName> {
355 let Some(parent) = function
356 .parent()
357 .filter(|node| node.kind() == "decorated_definition")
358 else {
359 return Vec::new();
360 };
361 let mut cursor = parent.walk();
362 parent
363 .named_children(&mut cursor)
364 .filter(|child| child.kind() == "decorator")
365 .filter_map(decorator_callee)
366 .filter_map(|callee| match callee.kind() {
367 "identifier" => Some(PythonOverloadDecoratorName::Direct(
368 node_text(callee, source).trim().to_string(),
369 )),
370 "attribute" => {
371 let attribute = callee.child_by_field_name("attribute")?;
372 if node_text(attribute, source).trim() != "overload" {
373 return None;
374 }
375 let object = callee.child_by_field_name("object")?;
376 (object.kind() == "identifier").then(|| {
377 PythonOverloadDecoratorName::Namespace(
378 node_text(object, source).trim().to_string(),
379 )
380 })
381 }
382 _ => None,
383 })
384 .collect()
385 }
386
387 pub(crate) fn matches_overload_binding(&self, name: &PythonOverloadDecoratorName) -> bool {
388 match name {
389 PythonOverloadDecoratorName::Direct(name) => self.direct.contains(name),
390 PythonOverloadDecoratorName::Namespace(name) => self.namespaces.contains(name),
391 }
392 }
393}
394
395fn is_typing_module(module: &str) -> bool {
396 matches!(module, "typing" | "typing_extensions")
397}
398
399pub fn expression_name_node<'tree>(expression: Node<'tree>) -> Option<Node<'tree>> {
401 let mut current = expression;
402 loop {
403 match current.kind() {
404 "identifier" => return Some(current),
405 "attribute" => current = current.child_by_field_name("attribute")?,
406 "call" => current = current.child_by_field_name("function")?,
407 _ => return None,
408 }
409 }
410}
411
412pub fn python_keyword_argument_label(node: Node<'_>) -> bool {
420 node.kind() == "identifier"
421 && node.parent().is_some_and(|parent| {
422 parent.kind() == "keyword_argument" && parent.child_by_field_name("name") == Some(node)
423 })
424}
425
426pub fn decorator_callee<'tree>(decorator: Node<'tree>) -> Option<Node<'tree>> {
428 if decorator.kind() != "decorator" {
429 return None;
430 }
431 let mut cursor = decorator.walk();
434 let mut expression = decorator
435 .named_children(&mut cursor)
436 .find(|child| !child.is_extra())?;
437 while expression.kind() == "call" {
438 expression = expression.child_by_field_name("function")?;
439 }
440 Some(expression)
441}
442
443pub fn python_node_is_in_annotation(node: Node<'_>) -> bool {
446 let start = node.start_byte();
447 let end = node.end_byte();
448 let mut current = node;
449 while let Some(parent) = current.parent() {
450 let annotation = match parent.kind() {
451 "function_definition" => parent.child_by_field_name("return_type"),
452 "typed_parameter" | "typed_default_parameter" | "assignment" => {
453 parent.child_by_field_name("type")
454 }
455 _ => None,
456 };
457 if let Some(annotation) = annotation
458 && annotation.start_byte() <= start
459 && end <= annotation.end_byte()
460 {
461 return true;
462 }
463 current = parent;
464 }
465 false
466}
467
468pub fn python_deferred_annotation_identifier_ranges(
470 string: Node<'_>,
471 source: &str,
472 cancellation: Option<&CancellationToken>,
473) -> Option<Vec<Range>> {
474 let tree = python_deferred_annotation_tree(string, source, cancellation)?;
475
476 let mut ranges = Vec::new();
477 let mut stack = vec![tree.root_node()];
478 while let Some(current) = stack.pop() {
479 if cancellation.is_some_and(CancellationToken::is_cancelled) {
480 return None;
481 }
482 if current.kind() == "identifier" {
483 ranges.push(Range {
484 start_byte: current.start_byte(),
485 end_byte: current.end_byte(),
486 start_line: current.start_position().row + 1,
487 end_line: current.end_position().row + 1,
488 });
489 }
490 for index in (0..current.named_child_count()).rev() {
491 if let Some(child) = current.named_child(index) {
492 stack.push(child);
493 }
494 }
495 }
496 Some(ranges)
497}
498
499pub fn python_deferred_annotation_tree(
503 string: Node<'_>,
504 source: &str,
505 cancellation: Option<&CancellationToken>,
506) -> Option<Tree> {
507 if string.kind() != "string"
508 || string
509 .parent()
510 .is_some_and(|parent| parent.kind() == "concatenated_string")
511 || !python_node_is_in_annotation(string)
512 || python_string_is_literal_value(string, source)
513 {
514 return None;
515 }
516
517 let mut content = None;
518 for index in 0..string.named_child_count() {
519 let child = string.named_child(index)?;
520 match child.kind() {
521 "string_start" | "string_end" => {}
522 "string_content" if content.is_none() => content = Some(child),
523 _ => return None,
524 }
525 }
526 let content = content?;
527 let language = tree_sitter_python::LANGUAGE.into();
528 let tree = brokk_bifrost_core::analyzer::common::parse_source_range_with_cancellation(
529 &language,
530 source,
531 content.range(),
532 cancellation,
533 )?;
534 if tree.root_node().has_error() {
535 return None;
536 }
537 Some(tree)
538}
539
540fn python_string_is_literal_value(string: Node<'_>, source: &str) -> bool {
544 let start = string.start_byte();
545 let end = string.end_byte();
546 let mut current = string;
547 while let Some(parent) = current.parent() {
548 match parent.kind() {
549 "subscript" => {
550 let Some(value) = parent.child_by_field_name("value") else {
551 return false;
552 };
553 if value.start_byte() <= start && end <= value.end_byte() {
554 return false;
555 }
556 return python_literal_annotation_base(value, source);
557 }
558 "generic_type" => {
559 let Some(value) = parent.named_child(0) else {
560 return false;
561 };
562 return python_literal_annotation_base(value, source);
563 }
564 _ => current = parent,
565 }
566 }
567 false
568}
569
570fn python_literal_annotation_base(value: Node<'_>, source: &str) -> bool {
571 match value.kind() {
572 "identifier" => node_text(value, source) == "Literal",
573 "attribute" => {
574 let (Some(object), Some(attribute)) = (
575 value.child_by_field_name("object"),
576 value.child_by_field_name("attribute"),
577 ) else {
578 return false;
579 };
580 object.kind() == "identifier"
581 && matches!(node_text(object, source), "typing" | "typing_extensions")
582 && attribute.kind() == "identifier"
583 && node_text(attribute, source) == "Literal"
584 }
585 "member_type" => {
586 let mut identifiers = Vec::new();
587 let mut stack = vec![value];
588 while let Some(node) = stack.pop() {
589 if node.kind() == "identifier" {
590 identifiers.push(node_text(node, source));
591 continue;
592 }
593 for index in (0..node.named_child_count()).rev() {
594 if let Some(child) = node.named_child(index) {
595 stack.push(child);
596 }
597 }
598 }
599 matches!(
600 identifiers.as_slice(),
601 ["typing", "Literal"] | ["typing_extensions", "Literal"]
602 )
603 }
604 _ => false,
605 }
606}
607
608pub(crate) fn python_contains_all_identifier(node: Node<'_>, source: &str) -> bool {
611 use brokk_bifrost_core::analyzer::tree_walk::{WalkControl, walk_named_tree_preorder};
612 let mut found = false;
613 walk_named_tree_preorder(node, true, |node| {
614 if node.kind() == "identifier" && &source[node.byte_range()] == "__all__" {
615 found = true;
616 WalkControl::Break
617 } else {
618 WalkControl::Continue
619 }
620 });
621 found
622}
623
624#[cfg(test)]
625mod tests {
626 use brokk_bifrost_test_macros::hardwon;
627
628 #[test]
629 fn qs6_python_syntax_field_boundary() {
630 use super::*;
631 for (source, expected) in [
632 ("sys.argv[1]", Some("sys.argv")),
633 ("obj.member[key]", Some("obj.member")),
634 ("obj.nested.member[key]", None),
635 ("values[key]", None),
636 ("obj.member", None),
637 ] {
638 let tree = crate::declarations::parse_python_tree(source).unwrap();
639 assert!(!tree.root_node().has_error());
640 let expression = tree
641 .root_node()
642 .named_child(0)
643 .unwrap()
644 .named_child(0)
645 .unwrap();
646 let seed = crate::runtime_values::python_runtime_keyed_access_seed(expression);
647 assert_eq!(seed.map(|node| &source[node.byte_range()]), expected);
648 if let Some(seed) = seed {
649 assert_eq!(seed.kind(), "attribute");
650 assert_eq!(
651 expression.child_by_field_name("value").unwrap().id(),
652 seed.id()
653 );
654 assert_eq!(seed.child_by_field_name("object").unwrap().start_byte(), 0);
655 assert_eq!(seed.start_byte(), 0);
656 assert_eq!(seed.end_byte(), expected.unwrap().len());
657 }
658 }
659 for (annotation, names) in [
660 ("pkg.Type", vec!["pkg", "Type"]),
661 ("pkg.Outer.Inner[int]", vec!["pkg", "Outer", "Inner"]),
662 ("Outer[T].Inner", vec!["Outer", "Inner"]),
663 ] {
664 let source = format!("def f() -> {annotation}: pass\n");
665 let tree = crate::declarations::parse_python_tree(&source).unwrap();
666 assert!(!tree.root_node().has_error());
667 let annotation_node = tree
668 .root_node()
669 .named_child(0)
670 .unwrap()
671 .child_by_field_name("return_type")
672 .unwrap();
673 let path = python_static_type_path(annotation_node).unwrap();
674 assert_eq!(
675 path.iter()
676 .map(|node| &source[node.byte_range()])
677 .collect::<Vec<_>>(),
678 names
679 );
680 for node in path {
681 assert_eq!(node.kind(), "identifier");
682 assert!(node.start_byte() >= annotation_node.start_byte());
683 assert!(node.end_byte() <= annotation_node.end_byte());
684 }
685 }
686 let source = "@factory()()\ndef f(): pass\n";
687 let tree = crate::declarations::parse_python_tree(source).unwrap();
688 let decorator = tree
689 .root_node()
690 .named_child(0)
691 .unwrap()
692 .named_child(0)
693 .unwrap();
694 assert_eq!(decorator.kind(), "decorator");
695 let call = decorator.named_child(0).unwrap();
696 assert_eq!(call.kind(), "call");
697 assert_eq!(call.child_by_field_name("function").unwrap().kind(), "call");
698 let callee = decorator_callee(decorator).unwrap();
699 assert_eq!(callee.kind(), "identifier");
700 assert_eq!(callee.byte_range(), 1..8);
701 assert_eq!(&source[callee.byte_range()], "factory");
702 }
703
704 #[test]
705 fn qs6_deferred_annotation_live_token_ranges() {
706 use super::*;
707 let source = "# \u{e9}\ndef f(x: \"pkg.Type[Other]\") -> \"Result\": pass\nordinary = \"text\"\ny: Literal[\"value\"]\n";
708 let tree = crate::declarations::parse_python_tree(source).unwrap();
709 assert!(!tree.root_node().has_error());
710 let fresh = CancellationToken::new();
711 let cancelled = CancellationToken::new();
712 cancelled.cancel();
713 let mut pending = vec![tree.root_node()];
714 let mut strings = Vec::new();
715 while let Some(node) = pending.pop() {
716 if node.kind() == "string" {
717 strings.push(node);
718 }
719 for index in (0..node.named_child_count()).rev() {
720 pending.push(node.named_child(index).unwrap());
721 }
722 }
723 assert_eq!(strings.len(), 4);
724 let mut names = Vec::new();
725 for string in strings {
726 let no_token = python_deferred_annotation_identifier_ranges(string, source, None);
727 assert_eq!(
728 python_deferred_annotation_identifier_ranges(string, source, Some(&fresh)),
729 no_token
730 );
731 assert!(
732 python_deferred_annotation_identifier_ranges(string, source, Some(&cancelled))
733 .is_none()
734 );
735 if let Some(ranges) = no_token {
736 for range in ranges {
737 assert!(range.start_byte > string.start_byte());
738 assert!(range.end_byte < string.end_byte());
739 assert_eq!(range.start_line, 2);
740 assert_eq!(range.end_line, 2);
741 names.push(&source[range.start_byte..range.end_byte]);
742 }
743 }
744 }
745 assert_eq!(names, ["pkg", "Type", "Other", "Result"]);
746 }
747
748 #[test]
749 #[hardwon("#5044: commented parentheses must preserve import identity and budget exhaustion")]
750 fn static_paths_keep_parentheses_comments_and_exhaustion_distinct() {
751 let source = "( # receiver\n module).function\n";
752 let tree = crate::declarations::parse_python_tree(source).unwrap();
753 let expression = tree
754 .root_node()
755 .named_child(0)
756 .unwrap()
757 .named_child(0)
758 .unwrap();
759 assert!(super::python_static_attribute_path(expression).is_none());
761 assert!(super::python_static_value_path_bounded(expression, || false).is_none());
762 let path = super::python_static_value_path_bounded(expression, || true)
763 .unwrap()
764 .unwrap();
765 assert_eq!(
766 path.iter()
767 .map(|node| node.utf8_text(source.as_bytes()).unwrap())
768 .collect::<Vec<_>>(),
769 ["module", "function"]
770 );
771 let dynamic = crate::declarations::parse_python_tree("factory().function\n").unwrap();
772 let expression = dynamic
773 .root_node()
774 .named_child(0)
775 .unwrap()
776 .named_child(0)
777 .unwrap();
778 assert!(matches!(
779 super::python_static_value_path_bounded(expression, || true),
780 Some(None)
781 ));
782 }
783}