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