Skip to main content

brokk_bifrost_python/
adapter.rs

1//! The Python answers behind `PythonAdapter`.
2//!
3//! `LanguageAdapter` is analysis-owned, so the trait impl itself stays in
4//! `analyzer/python/adapter.rs`; every answer it gives comes from here or from
5//! [`crate::declarations`]. `synthesize_hydrated_units` is the one exception:
6//! it mutates `FileState`, an analysis type, and stays with the shell.
7
8use brokk_bifrost_core::analyzer::cognitive_complexity;
9use std::sync::LazyLock;
10
11use crate::declarations::python_is_decorated_function_boundary;
12
13pub const PYTHON_FILE_EXTENSION: &str = "py";
14
15/// Tree-sitter node-kind mapping used by the cognitive-complexity scorer
16/// for Python. Mirrors `ai.brokk.analyzer.python.CognitiveComplexityAnalysis`.
17pub static PYTHON_COGNITIVE_CONFIG: LazyLock<cognitive_complexity::Config> =
18    LazyLock::new(|| cognitive_complexity::Config {
19        if_types: &["if_statement"],
20        alternate_if_types: &["elif_clause"],
21        loop_types: &["for_statement", "while_statement"],
22        catch_types: &["except_clause"],
23        conditional_types: &["conditional_expression"],
24        case_types: &["case_clause"],
25        binary_types: &["boolean_operator"],
26        logical_operators: &["and", "or"],
27        named_function_boundary_types: &["function_definition"],
28        anonymous_function_types: &["lambda"],
29        named_function_boundary_predicate: Some(python_is_decorated_function_boundary),
30        ..cognitive_complexity::Config::empty()
31    });
32
33pub fn python_extract_call_receiver(reference: &str) -> Option<String> {
34    brokk_bifrost_core::analyzer::common::extract_call_reference_parts(reference, &["."])
35        .map(|(receiver, _)| receiver.to_string())
36}
37
38/// Python data literals. Bytes and implicit concatenation use the same grammar
39/// nodes; replacement fields remain executable structure.
40pub const PYTHON_LINE_DATA_KINDS: brokk_bifrost_core::analyzer::tree_walk::LineDataKinds =
41    brokk_bifrost_core::analyzer::tree_walk::LineDataKinds {
42        literals: &["string", "concatenated_string", "integer", "float"],
43        interpolations: &["interpolation"],
44        composites: &[
45            ("list", &["[", "]", ","]),
46            ("tuple", &["(", ")", ","]),
47            ("unary_operator", &["-", "+"]),
48        ],
49    };
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use std::ops::ControlFlow;
55
56    #[test]
57    fn qs6_string_data_grammar_and_cancellation() {
58        for (literal, kind, interpolations, data) in [
59            ("u'caf\u{e9}'", "string", 0, true),
60            ("b'bytes'", "string", 0, true),
61            ("'one' 'two'", "concatenated_string", 0, true),
62            ("f'plain {{braces}}'", "string", 0, true),
63            ("f'{value:{width}}'", "string", 1, false),
64            ("'prefix' f'{value}'", "concatenated_string", 1, false),
65        ] {
66            let source = format!("value = {literal}\n");
67            let tree = crate::declarations::parse_python_tree(&source).unwrap();
68            let root = tree.root_node();
69            assert!(!root.has_error(), "{}", root.to_sexp());
70            let value = root
71                .named_child(0)
72                .unwrap()
73                .named_child(0)
74                .unwrap()
75                .child_by_field_name("right")
76                .unwrap();
77            assert_eq!(value.kind(), kind);
78            let mut stack = vec![value];
79            let mut count = 0;
80            while let Some(node) = stack.pop() {
81                assert!(!node.is_error() && !node.is_missing());
82                count += usize::from(node.kind() == "interpolation");
83                let mut cursor = node.walk();
84                stack.extend(node.named_children(&mut cursor));
85            }
86            assert_eq!(count, interpolations, "{}", value.to_sexp());
87            let data_subtrees = |should_stop: &mut dyn FnMut() -> bool| {
88                match brokk_bifrost_core::analyzer::tree_walk::maximal_data_subtrees(
89                    value,
90                    &PYTHON_LINE_DATA_KINDS,
91                    |_| true,
92                    should_stop,
93                ) {
94                    ControlFlow::Continue(subtrees) => ControlFlow::Continue(
95                        subtrees
96                            .iter()
97                            .any(|subtree| subtree.range == value.byte_range()),
98                    ),
99                    ControlFlow::Break(()) => ControlFlow::Break(()),
100                }
101            };
102            assert_eq!(data_subtrees(&mut || false), ControlFlow::Continue(data));
103            let mut stops = 0;
104            assert_eq!(
105                data_subtrees(&mut || {
106                    stops += 1;
107                    true
108                }),
109                ControlFlow::Break(())
110            );
111            assert_eq!(stops, 1);
112        }
113    }
114}