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