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_parameter_name(node: Node<'_>, source: &str) -> Option<String> {
8 python_parameter_name_bounded(node, source, || true)
9 .expect("unbounded parameter name lookup completes")
10}
11
12pub(crate) fn python_parameter_name_bounded(
13 node: Node<'_>,
14 source: &str,
15 scope_step: impl FnMut() -> bool,
16) -> Option<Option<String>> {
17 python_parameter_node_bounded(node, scope_step).map(|node| {
18 node.and_then(|node| {
19 let name = node_text(node, source).trim();
20 (!name.is_empty()).then(|| name.to_owned())
21 })
22 })
23}
24
25pub(crate) fn python_parameter_node(node: Node<'_>) -> Option<Node<'_>> {
27 python_parameter_node_bounded(node, || true).expect("unbounded parameter token lookup")
28}
29
30fn python_parameter_node_bounded(
31 mut node: Node<'_>,
32 mut scope_step: impl FnMut() -> bool,
33) -> Option<Option<Node<'_>>> {
34 loop {
35 if !scope_step() {
36 return None;
37 }
38 match node.kind() {
39 "identifier" => return Some((!node.is_missing()).then_some(node)),
40 "typed_parameter"
41 | "typed_default_parameter"
42 | "default_parameter"
43 | "list_splat_pattern"
44 | "dictionary_splat_pattern" => {
45 if let Some(name) = node.child_by_field_name("name") {
46 node = name;
47 continue;
48 }
49 let mut selected = None;
50 let mut cursor = node.walk();
51 for child in node.named_children(&mut cursor) {
52 if !scope_step() {
53 return None;
54 }
55 if matches!(
56 child.kind(),
57 "identifier" | "list_splat_pattern" | "dictionary_splat_pattern"
58 ) {
59 selected = Some(child);
60 break;
61 }
62 }
63 let Some(name) = selected else {
64 return Some(None);
65 };
66 node = name;
67 }
68 _ => return Some(None),
69 }
70 }
71}
72
73pub fn leftmost_identifier(mut node: Node<'_>) -> Option<Node<'_>> {
74 loop {
75 match node.kind() {
76 "identifier" => return Some(node),
77 "attribute" => node = node.child_by_field_name("object")?,
78 _ => return None,
79 }
80 }
81}
82
83pub(crate) fn attribute_chain<'tree>(node: Node<'tree>) -> Option<(Node<'tree>, Vec<Node<'tree>>)> {
84 let mut attributes = Vec::new();
85 let mut current = node;
86 loop {
87 if current.kind() != "attribute" {
88 return None;
89 }
90 attributes.push(current.child_by_field_name("attribute")?);
91 current = current.child_by_field_name("object")?;
92 if current.kind() == "identifier" {
93 attributes.reverse();
94 return Some((current, attributes));
95 }
96 }
97}
98
99pub fn python_reference_is_deferred_function_body(node: Node<'_>) -> bool {
100 python_reference_is_deferred_function_body_bounded(node, || true)
101 .expect("unbounded deferred-context lookup completes")
102}
103
104pub fn python_reference_is_deferred_function_body_bounded(
106 node: Node<'_>,
107 mut scope_step: impl FnMut() -> bool,
108) -> Option<bool> {
109 let site_start = node.start_byte();
110 let site_end = node.end_byte();
111 let mut current = node;
112 while let Some(parent) = current.parent() {
113 if !scope_step() {
114 return None;
115 }
116 if matches!(parent.kind(), "function_definition" | "lambda")
117 && parent
118 .child_by_field_name("body")
119 .is_some_and(|body| body.start_byte() <= site_start && site_end <= body.end_byte())
120 {
121 return Some(true);
122 }
123 current = parent;
124 }
125 Some(false)
126}
127
128pub fn python_static_attribute_path<'tree>(node: Node<'tree>) -> Option<Vec<Node<'tree>>> {
134 python_static_value_path_with(node, || true, |_| None)
135 .expect("unbounded static path lookup completes")
136}
137
138pub fn python_static_attribute_path_bounded<'tree>(
141 node: Node<'tree>,
142 scope_step: impl FnMut() -> bool,
143) -> Option<Vec<Node<'tree>>> {
144 python_static_value_path_with(node, scope_step, |_| None).flatten()
145}
146
147pub fn python_static_value_path_bounded<'tree>(
150 node: Node<'tree>,
151 scope_step: impl FnMut() -> bool,
152) -> Option<Option<Vec<Node<'tree>>>> {
153 python_static_value_path_with(
154 node,
155 scope_step,
156 crate::structural::python_parenthesized_expression_child,
157 )
158}
159
160fn python_static_value_path_with<'tree>(
161 mut node: Node<'tree>,
162 mut scope_step: impl FnMut() -> bool,
163 mut parenthesized_child: impl FnMut(Node<'tree>) -> Option<Node<'tree>>,
164) -> Option<Option<Vec<Node<'tree>>>> {
165 let mut path = Vec::new();
166 loop {
167 if !scope_step() {
168 return None;
169 }
170 match node.kind() {
171 "identifier" => {
172 path.push(node);
173 break;
174 }
175 "attribute" => {
176 let Some(object) = push_static_attribute_segment(node, &mut path) else {
177 return Some(None);
178 };
179 node = object;
180 }
181 "parenthesized_expression" => {
182 for _ in brokk_bifrost_core::analyzer::tree_walk::named_children_iter(node) {
185 if !scope_step() {
186 return None;
187 }
188 }
189 let Some(child) = parenthesized_child(node) else {
190 return Some(None);
191 };
192 node = child;
193 }
194 _ => return Some(None),
195 }
196 }
197 for _ in 0..path.len() {
198 if !scope_step() {
199 return None;
200 }
201 }
202 path.reverse();
203 Some(Some(path))
204}
205
206pub fn python_static_type_path<'tree>(mut node: Node<'tree>) -> Option<Vec<Node<'tree>>> {
210 let mut path = Vec::new();
211 loop {
212 match node.kind() {
213 "identifier" => {
214 path.push(node);
215 break;
216 }
217 "type" | "generic_type" | "subscript" => node = node.named_child(0)?,
218 "attribute" => {
219 node = push_static_attribute_segment(node, &mut path)?;
220 }
221 "member_type" => {
222 let mut cursor = node.walk();
223 let mut children = node.named_children(&mut cursor);
224 let qualifier = children.next()?;
225 let member = children.next()?;
226 if member.kind() != "identifier" || children.next().is_some() {
227 return None;
228 }
229 path.push(member);
230 node = qualifier;
231 }
232 _ => return None,
233 }
234 }
235 path.reverse();
236 Some(path)
237}
238
239fn push_static_attribute_segment<'tree>(
240 node: Node<'tree>,
241 path: &mut Vec<Node<'tree>>,
242) -> Option<Node<'tree>> {
243 let attribute = node.child_by_field_name("attribute")?;
244 if attribute.kind() != "identifier" {
245 return None;
246 }
247 path.push(attribute);
248 node.child_by_field_name("object")
249}
250
251pub fn python_plain_string_literal<'source>(
257 node: Node<'_>,
258 source: &'source str,
259) -> Option<&'source str> {
260 if node.kind() != "string"
261 || node
262 .parent()
263 .is_some_and(|parent| parent.kind() == "concatenated_string")
264 {
265 return None;
266 }
267 let mut content = None;
268 let mut cursor = node.walk();
269 for child in node.named_children(&mut cursor) {
270 match child.kind() {
271 "string_start" | "string_end" => {
272 let delimiter = child.utf8_text(source.as_bytes()).ok()?;
273 if delimiter
274 .chars()
275 .any(|character| character != '"' && character != '\'')
276 {
277 return None;
278 }
279 }
280 "string_content" if child.named_child_count() == 0 && content.is_none() => {
281 content = Some(child);
282 }
283 _ => return None,
284 }
285 }
286 Some(content.map_or("", |child| {
287 child
288 .utf8_text(source.as_bytes())
289 .expect("a tree-sitter node range is valid UTF-8 source")
290 }))
291}
292
293#[derive(Debug, Default)]
294pub struct PythonOverloadDecoratorBindings {
295 direct: HashSet<String>,
296 namespaces: HashSet<String>,
297}
298
299#[derive(Debug)]
300pub(crate) enum PythonOverloadDecoratorName {
301 Direct(String),
302 Namespace(String),
303}
304
305impl PythonOverloadDecoratorBindings {
306 pub(crate) fn collect_import(
309 &mut self,
310 import: &brokk_bifrost_core::analyzer::model::ImportInfo,
311 ) {
312 use brokk_bifrost_core::analyzer::model::StructuredImportPathKind;
313 let Some(path) = &import.path else { return };
314 match (path.kind, path.segments.as_slice()) {
315 (Some(StructuredImportPathKind::Namespace), [module]) if is_typing_module(module) => {
316 self.namespaces
317 .insert(import.alias.as_ref().unwrap_or(module).clone());
318 }
319 (Some(StructuredImportPathKind::ImportFrom), [module, name])
320 if is_typing_module(module) && name == "overload" =>
321 {
322 self.direct
323 .insert(import.alias.as_ref().unwrap_or(name).clone());
324 }
325 _ => {}
326 }
327 }
328
329 pub(crate) fn overload_decorator_names(
334 function: Node<'_>,
335 source: &str,
336 ) -> Vec<PythonOverloadDecoratorName> {
337 let Some(parent) = function
338 .parent()
339 .filter(|node| node.kind() == "decorated_definition")
340 else {
341 return Vec::new();
342 };
343 let mut cursor = parent.walk();
344 parent
345 .named_children(&mut cursor)
346 .filter(|child| child.kind() == "decorator")
347 .filter_map(decorator_callee)
348 .filter_map(|callee| match callee.kind() {
349 "identifier" => Some(PythonOverloadDecoratorName::Direct(
350 node_text(callee, source).trim().to_string(),
351 )),
352 "attribute" => {
353 let attribute = callee.child_by_field_name("attribute")?;
354 if node_text(attribute, source).trim() != "overload" {
355 return None;
356 }
357 let object = callee.child_by_field_name("object")?;
358 (object.kind() == "identifier").then(|| {
359 PythonOverloadDecoratorName::Namespace(
360 node_text(object, source).trim().to_string(),
361 )
362 })
363 }
364 _ => None,
365 })
366 .collect()
367 }
368
369 pub(crate) fn matches_overload_binding(&self, name: &PythonOverloadDecoratorName) -> bool {
370 match name {
371 PythonOverloadDecoratorName::Direct(name) => self.direct.contains(name),
372 PythonOverloadDecoratorName::Namespace(name) => self.namespaces.contains(name),
373 }
374 }
375}
376
377fn is_typing_module(module: &str) -> bool {
378 matches!(module, "typing" | "typing_extensions")
379}
380
381pub fn expression_name_node<'tree>(expression: Node<'tree>) -> Option<Node<'tree>> {
383 let mut current = expression;
384 loop {
385 match current.kind() {
386 "identifier" => return Some(current),
387 "attribute" => current = current.child_by_field_name("attribute")?,
388 "call" => current = current.child_by_field_name("function")?,
389 _ => return None,
390 }
391 }
392}
393
394pub fn python_keyword_argument_label(node: Node<'_>) -> bool {
402 node.kind() == "identifier"
403 && node.parent().is_some_and(|parent| {
404 parent.kind() == "keyword_argument" && parent.child_by_field_name("name") == Some(node)
405 })
406}
407
408pub fn decorator_callee<'tree>(decorator: Node<'tree>) -> Option<Node<'tree>> {
410 if decorator.kind() != "decorator" {
411 return None;
412 }
413 let mut cursor = decorator.walk();
416 let mut expression = decorator
417 .named_children(&mut cursor)
418 .find(|child| !child.is_extra())?;
419 while expression.kind() == "call" {
420 expression = expression.child_by_field_name("function")?;
421 }
422 Some(expression)
423}
424
425pub fn python_node_is_in_annotation(node: Node<'_>) -> bool {
428 let start = node.start_byte();
429 let end = node.end_byte();
430 let mut current = node;
431 while let Some(parent) = current.parent() {
432 let annotation = match parent.kind() {
433 "function_definition" => parent.child_by_field_name("return_type"),
434 "typed_parameter" | "typed_default_parameter" | "assignment" => {
435 parent.child_by_field_name("type")
436 }
437 _ => None,
438 };
439 if let Some(annotation) = annotation
440 && annotation.start_byte() <= start
441 && end <= annotation.end_byte()
442 {
443 return true;
444 }
445 current = parent;
446 }
447 false
448}
449
450pub fn python_deferred_annotation_identifier_ranges(
452 string: Node<'_>,
453 source: &str,
454 cancellation: Option<&CancellationToken>,
455) -> Option<Vec<Range>> {
456 let tree = python_deferred_annotation_tree(string, source, cancellation)?;
457
458 let mut ranges = Vec::new();
459 let mut stack = vec![tree.root_node()];
460 while let Some(current) = stack.pop() {
461 if cancellation.is_some_and(CancellationToken::is_cancelled) {
462 return None;
463 }
464 if current.kind() == "identifier" {
465 ranges.push(Range {
466 start_byte: current.start_byte(),
467 end_byte: current.end_byte(),
468 start_line: current.start_position().row + 1,
469 end_line: current.end_position().row + 1,
470 });
471 }
472 for index in (0..current.named_child_count()).rev() {
473 if let Some(child) = current.named_child(index) {
474 stack.push(child);
475 }
476 }
477 }
478 Some(ranges)
479}
480
481pub fn python_deferred_annotation_tree(
485 string: Node<'_>,
486 source: &str,
487 cancellation: Option<&CancellationToken>,
488) -> Option<Tree> {
489 if string.kind() != "string"
490 || string
491 .parent()
492 .is_some_and(|parent| parent.kind() == "concatenated_string")
493 || !python_node_is_in_annotation(string)
494 || python_string_is_literal_value(string, source)
495 {
496 return None;
497 }
498
499 let mut content = None;
500 for index in 0..string.named_child_count() {
501 let child = string.named_child(index)?;
502 match child.kind() {
503 "string_start" | "string_end" => {}
504 "string_content" if content.is_none() => content = Some(child),
505 _ => return None,
506 }
507 }
508 let content = content?;
509 let language = tree_sitter_python::LANGUAGE.into();
510 let tree = brokk_bifrost_core::analyzer::common::parse_source_range_with_cancellation(
511 &language,
512 source,
513 content.range(),
514 cancellation,
515 )?;
516 if tree.root_node().has_error() {
517 return None;
518 }
519 Some(tree)
520}
521
522fn python_string_is_literal_value(string: Node<'_>, source: &str) -> bool {
526 let start = string.start_byte();
527 let end = string.end_byte();
528 let mut current = string;
529 while let Some(parent) = current.parent() {
530 match parent.kind() {
531 "subscript" => {
532 let Some(value) = parent.child_by_field_name("value") else {
533 return false;
534 };
535 if value.start_byte() <= start && end <= value.end_byte() {
536 return false;
537 }
538 return python_literal_annotation_base(value, source);
539 }
540 "generic_type" => {
541 let Some(value) = parent.named_child(0) else {
542 return false;
543 };
544 return python_literal_annotation_base(value, source);
545 }
546 _ => current = parent,
547 }
548 }
549 false
550}
551
552fn python_literal_annotation_base(value: Node<'_>, source: &str) -> bool {
553 match value.kind() {
554 "identifier" => node_text(value, source) == "Literal",
555 "attribute" => {
556 let (Some(object), Some(attribute)) = (
557 value.child_by_field_name("object"),
558 value.child_by_field_name("attribute"),
559 ) else {
560 return false;
561 };
562 object.kind() == "identifier"
563 && matches!(node_text(object, source), "typing" | "typing_extensions")
564 && attribute.kind() == "identifier"
565 && node_text(attribute, source) == "Literal"
566 }
567 "member_type" => {
568 let mut identifiers = Vec::new();
569 let mut stack = vec![value];
570 while let Some(node) = stack.pop() {
571 if node.kind() == "identifier" {
572 identifiers.push(node_text(node, source));
573 continue;
574 }
575 for index in (0..node.named_child_count()).rev() {
576 if let Some(child) = node.named_child(index) {
577 stack.push(child);
578 }
579 }
580 }
581 matches!(
582 identifiers.as_slice(),
583 ["typing", "Literal"] | ["typing_extensions", "Literal"]
584 )
585 }
586 _ => false,
587 }
588}
589
590pub(crate) fn python_contains_all_identifier(node: Node<'_>, source: &str) -> bool {
593 use brokk_bifrost_core::analyzer::tree_walk::{WalkControl, walk_named_tree_preorder};
594 let mut found = false;
595 walk_named_tree_preorder(node, true, |node| {
596 if node.kind() == "identifier" && &source[node.byte_range()] == "__all__" {
597 found = true;
598 WalkControl::Break
599 } else {
600 WalkControl::Continue
601 }
602 });
603 found
604}
605
606#[cfg(test)]
607mod tests {
608 use brokk_bifrost_test_macros::hardwon;
609
610 #[test]
611 #[hardwon("#5044: commented parentheses must preserve import identity and budget exhaustion")]
612 fn static_paths_keep_parentheses_comments_and_exhaustion_distinct() {
613 let source = "( # receiver\n module).function\n";
614 let tree = crate::declarations::parse_python_tree(source).unwrap();
615 let expression = tree
616 .root_node()
617 .named_child(0)
618 .unwrap()
619 .named_child(0)
620 .unwrap();
621 assert!(super::python_static_attribute_path(expression).is_none());
623 assert!(super::python_static_value_path_bounded(expression, || false).is_none());
624 let path = super::python_static_value_path_bounded(expression, || true)
625 .unwrap()
626 .unwrap();
627 assert_eq!(
628 path.iter()
629 .map(|node| node.utf8_text(source.as_bytes()).unwrap())
630 .collect::<Vec<_>>(),
631 ["module", "function"]
632 );
633 let dynamic = crate::declarations::parse_python_tree("factory().function\n").unwrap();
634 let expression = dynamic
635 .root_node()
636 .named_child(0)
637 .unwrap()
638 .named_child(0)
639 .unwrap();
640 assert!(matches!(
641 super::python_static_value_path_bounded(expression, || true),
642 Some(None)
643 ));
644 }
645}