Skip to main content

brokk_bifrost_python/
syntax.rs

1use brokk_bifrost_core::analyzer::Range;
2use brokk_bifrost_core::cancellation::CancellationToken;
3use brokk_bifrost_core::hash::HashSet;
4use tree_sitter::{Node, Tree};
5
6#[derive(Debug, Default)]
7pub struct PythonOverloadDecoratorBindings {
8    direct: HashSet<String>,
9    namespaces: HashSet<String>,
10}
11
12impl PythonOverloadDecoratorBindings {
13    pub fn collect(root: Node<'_>, source: &str) -> Self {
14        let mut bindings = Self::default();
15        let mut stack = vec![root];
16
17        while let Some(node) = stack.pop() {
18            match node.kind() {
19                "function_definition" | "class_definition" | "lambda" => continue,
20                "import_statement" => bindings.collect_namespace_imports(node, source),
21                "import_from_statement" => bindings.collect_direct_imports(node, source),
22                _ => {}
23            }
24
25            let mut cursor = node.walk();
26            let children: Vec<_> = node.named_children(&mut cursor).collect();
27            stack.extend(children.into_iter().rev());
28        }
29
30        bindings
31    }
32
33    fn collect_namespace_imports(&mut self, node: Node<'_>, source: &str) {
34        let mut cursor = node.walk();
35        for imported in node.children_by_field_name("name", &mut cursor) {
36            match imported.kind() {
37                "dotted_name" => {
38                    let module = node_text(imported, source).trim();
39                    if is_typing_module(module) {
40                        self.namespaces.insert(module.to_string());
41                    }
42                }
43                "aliased_import" => {
44                    let Some(name) = imported.child_by_field_name("name") else {
45                        continue;
46                    };
47                    if !is_typing_module(node_text(name, source).trim()) {
48                        continue;
49                    }
50                    let Some(alias) = imported.child_by_field_name("alias") else {
51                        continue;
52                    };
53                    let alias = node_text(alias, source).trim();
54                    if !alias.is_empty() {
55                        self.namespaces.insert(alias.to_string());
56                    }
57                }
58                _ => {}
59            }
60        }
61    }
62
63    fn collect_direct_imports(&mut self, node: Node<'_>, source: &str) {
64        let Some(module) = node.child_by_field_name("module_name") else {
65            return;
66        };
67        if !is_typing_module(node_text(module, source).trim()) {
68            return;
69        }
70
71        let mut cursor = node.walk();
72        for imported in node.children_by_field_name("name", &mut cursor) {
73            match imported.kind() {
74                "dotted_name" if node_text(imported, source).trim() == "overload" => {
75                    self.direct.insert("overload".to_string());
76                }
77                "aliased_import" => {
78                    let Some(name) = imported.child_by_field_name("name") else {
79                        continue;
80                    };
81                    if node_text(name, source).trim() != "overload" {
82                        continue;
83                    }
84                    let Some(alias) = imported.child_by_field_name("alias") else {
85                        continue;
86                    };
87                    let alias = node_text(alias, source).trim();
88                    if !alias.is_empty() {
89                        self.direct.insert(alias.to_string());
90                    }
91                }
92                _ => {}
93            }
94        }
95    }
96
97    pub fn decorates_as_overload(&self, function: Node<'_>, source: &str) -> bool {
98        let Some(parent) = function
99            .parent()
100            .filter(|node| node.kind() == "decorated_definition")
101        else {
102            return false;
103        };
104
105        let mut cursor = parent.walk();
106        parent
107            .named_children(&mut cursor)
108            .filter(|child| child.kind() == "decorator")
109            .filter_map(decorator_callee)
110            .any(|callee| match callee.kind() {
111                "identifier" => self.direct.contains(node_text(callee, source).trim()),
112                "attribute" => {
113                    let Some(attribute) = callee.child_by_field_name("attribute") else {
114                        return false;
115                    };
116                    if node_text(attribute, source).trim() != "overload" {
117                        return false;
118                    }
119                    let Some(object) = callee.child_by_field_name("object") else {
120                        return false;
121                    };
122                    object.kind() == "identifier"
123                        && self.namespaces.contains(node_text(object, source).trim())
124                }
125                _ => false,
126            })
127    }
128}
129
130fn is_typing_module(module: &str) -> bool {
131    matches!(module, "typing" | "typing_extensions")
132}
133
134fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
135    brokk_bifrost_core::analyzer::common::node_source_text(node, source)
136}
137
138/// Return the name-bearing node of a Python expression using tree-sitter fields.
139pub fn expression_name_node<'tree>(expression: Node<'tree>) -> Option<Node<'tree>> {
140    let mut current = expression;
141    loop {
142        match current.kind() {
143            "identifier" => return Some(current),
144            "attribute" => current = current.child_by_field_name("attribute")?,
145            "call" => current = current.child_by_field_name("function")?,
146            _ => return None,
147        }
148    }
149}
150
151/// Whether `node` is the label of a keyword argument: the `x` in `f(x=1)`.
152///
153/// The label names a parameter or member at the CALLEE, selected by the call's
154/// target, not by anything bound where the label is written. That is why the
155/// occurrence-role adapter in `structural.rs` classifies it `LabelOrKey`, whose
156/// occurrence class is `NonReference`, and it is the rule the reference census
157/// reads to decide that the label is not a forward-reference probe (#2054).
158pub fn python_keyword_argument_label(node: Node<'_>) -> bool {
159    node.kind() == "identifier"
160        && node.parent().is_some_and(|parent| {
161            parent.kind() == "keyword_argument" && parent.child_by_field_name("name") == Some(node)
162        })
163}
164
165/// Return a decorator's callable expression, peeling an optional invocation.
166pub fn decorator_callee<'tree>(decorator: Node<'tree>) -> Option<Node<'tree>> {
167    if decorator.kind() != "decorator" {
168        return None;
169    }
170    let mut expression = decorator.named_child(0)?;
171    while expression.kind() == "call" {
172        expression = expression.child_by_field_name("function")?;
173    }
174    Some(expression)
175}
176
177/// Whether `node` is contained by a parser field that Python evaluates as an
178/// annotation rather than as an ordinary expression.
179pub fn python_node_is_in_annotation(node: Node<'_>) -> bool {
180    let start = node.start_byte();
181    let end = node.end_byte();
182    let mut current = node;
183    while let Some(parent) = current.parent() {
184        let annotation = match parent.kind() {
185            "function_definition" => parent.child_by_field_name("return_type"),
186            "typed_parameter" | "typed_default_parameter" | "assignment" => {
187                parent.child_by_field_name("type")
188            }
189            _ => None,
190        };
191        if let Some(annotation) = annotation
192            && annotation.start_byte() <= start
193            && end <= annotation.end_byte()
194        {
195            return true;
196        }
197        current = parent;
198    }
199    false
200}
201
202/// Parse one exactly mapped deferred annotation and return its identifier ranges.
203pub fn python_deferred_annotation_identifier_ranges(
204    string: Node<'_>,
205    source: &str,
206    cancellation: Option<&CancellationToken>,
207) -> Option<Vec<Range>> {
208    let tree = python_deferred_annotation_tree(string, source, cancellation)?;
209
210    let mut ranges = Vec::new();
211    let mut stack = vec![tree.root_node()];
212    while let Some(current) = stack.pop() {
213        if cancellation.is_some_and(CancellationToken::is_cancelled) {
214            return None;
215        }
216        if current.kind() == "identifier" {
217            ranges.push(Range {
218                start_byte: current.start_byte(),
219                end_byte: current.end_byte(),
220                start_line: current.start_position().row + 1,
221                end_line: current.end_position().row + 1,
222            });
223        }
224        for index in (0..current.named_child_count()).rev() {
225            if let Some(child) = current.named_child(index) {
226                stack.push(child);
227            }
228        }
229    }
230    Some(ranges)
231}
232
233/// Parse one quoted annotation expression while preserving its original source
234/// byte coordinates. Literal string values and arbitrary strings are rejected
235/// by the same structured gate used by inverse membership.
236pub fn python_deferred_annotation_tree(
237    string: Node<'_>,
238    source: &str,
239    cancellation: Option<&CancellationToken>,
240) -> Option<Tree> {
241    if string.kind() != "string"
242        || string
243            .parent()
244            .is_some_and(|parent| parent.kind() == "concatenated_string")
245        || !python_node_is_in_annotation(string)
246        || python_string_is_literal_value(string, source)
247    {
248        return None;
249    }
250
251    let mut content = None;
252    for index in 0..string.named_child_count() {
253        let child = string.named_child(index)?;
254        match child.kind() {
255            "string_start" | "string_end" => {}
256            "string_content" if content.is_none() => content = Some(child),
257            _ => return None,
258        }
259    }
260    let content = content?;
261    let language = tree_sitter_python::LANGUAGE.into();
262    let tree = brokk_bifrost_core::analyzer::common::parse_source_range_with_cancellation(
263        &language,
264        source,
265        content.range(),
266        cancellation,
267    )?;
268    if tree.root_node().has_error() {
269        return None;
270    }
271    Some(tree)
272}
273
274/// Whether `string` is a value argument of `Literal[...]`, rather than a
275/// deferred type expression merely because the whole subscript is an
276/// annotation.
277fn python_string_is_literal_value(string: Node<'_>, source: &str) -> bool {
278    let start = string.start_byte();
279    let end = string.end_byte();
280    let mut current = string;
281    while let Some(parent) = current.parent() {
282        match parent.kind() {
283            "subscript" => {
284                let Some(value) = parent.child_by_field_name("value") else {
285                    return false;
286                };
287                if value.start_byte() <= start && end <= value.end_byte() {
288                    return false;
289                }
290                return python_literal_annotation_base(value, source);
291            }
292            "generic_type" => {
293                let Some(value) = parent.named_child(0) else {
294                    return false;
295                };
296                return python_literal_annotation_base(value, source);
297            }
298            _ => current = parent,
299        }
300    }
301    false
302}
303
304fn python_literal_annotation_base(value: Node<'_>, source: &str) -> bool {
305    match value.kind() {
306        "identifier" => node_text(value, source) == "Literal",
307        "attribute" => {
308            let (Some(object), Some(attribute)) = (
309                value.child_by_field_name("object"),
310                value.child_by_field_name("attribute"),
311            ) else {
312                return false;
313            };
314            object.kind() == "identifier"
315                && matches!(node_text(object, source), "typing" | "typing_extensions")
316                && attribute.kind() == "identifier"
317                && node_text(attribute, source) == "Literal"
318        }
319        "member_type" => {
320            let mut identifiers = Vec::new();
321            let mut stack = vec![value];
322            while let Some(node) = stack.pop() {
323                if node.kind() == "identifier" {
324                    identifiers.push(node_text(node, source));
325                    continue;
326                }
327                for index in (0..node.named_child_count()).rev() {
328                    if let Some(child) = node.named_child(index) {
329                        stack.push(child);
330                    }
331                }
332            }
333            matches!(
334                identifiers.as_slice(),
335                ["typing", "Literal"] | ["typing_extensions", "Literal"]
336            )
337        }
338        _ => false,
339    }
340}