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