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
123#[derive(Debug)]
124pub(crate) enum PythonOverloadDecoratorName {
125    Direct(String),
126    Namespace(String),
127}
128
129impl PythonOverloadDecoratorBindings {
130    /// Capture typing bindings from the import interpretation already owned by
131    /// the primary producer. Only module-level leaves reach this collector.
132    pub(crate) fn collect_import(
133        &mut self,
134        import: &brokk_bifrost_core::analyzer::model::ImportInfo,
135    ) {
136        use brokk_bifrost_core::analyzer::model::StructuredImportPathKind;
137        let Some(path) = &import.path else { return };
138        match (path.kind, path.segments.as_slice()) {
139            (Some(StructuredImportPathKind::Namespace), [module]) if is_typing_module(module) => {
140                self.namespaces
141                    .insert(import.alias.as_ref().unwrap_or(module).clone());
142            }
143            (Some(StructuredImportPathKind::ImportFrom), [module, name])
144                if is_typing_module(module) && name == "overload" =>
145            {
146                self.direct
147                    .insert(import.alias.as_ref().unwrap_or(name).clone());
148            }
149            _ => {}
150        }
151    }
152
153    /// Return the exact decorator binding shapes used by one function. The
154    /// names are captured during the primary walk; the import binding set is
155    /// resolved after the walk so a later typing import is handled exactly as
156    /// the historical whole-file predicate did.
157    pub(crate) fn overload_decorator_names(
158        function: Node<'_>,
159        source: &str,
160    ) -> Vec<PythonOverloadDecoratorName> {
161        let Some(parent) = function
162            .parent()
163            .filter(|node| node.kind() == "decorated_definition")
164        else {
165            return Vec::new();
166        };
167        let mut cursor = parent.walk();
168        parent
169            .named_children(&mut cursor)
170            .filter(|child| child.kind() == "decorator")
171            .filter_map(decorator_callee)
172            .filter_map(|callee| match callee.kind() {
173                "identifier" => Some(PythonOverloadDecoratorName::Direct(
174                    node_text(callee, source).trim().to_string(),
175                )),
176                "attribute" => {
177                    let attribute = callee.child_by_field_name("attribute")?;
178                    if node_text(attribute, source).trim() != "overload" {
179                        return None;
180                    }
181                    let object = callee.child_by_field_name("object")?;
182                    (object.kind() == "identifier").then(|| {
183                        PythonOverloadDecoratorName::Namespace(
184                            node_text(object, source).trim().to_string(),
185                        )
186                    })
187                }
188                _ => None,
189            })
190            .collect()
191    }
192
193    pub(crate) fn matches_overload_binding(&self, name: &PythonOverloadDecoratorName) -> bool {
194        match name {
195            PythonOverloadDecoratorName::Direct(name) => self.direct.contains(name),
196            PythonOverloadDecoratorName::Namespace(name) => self.namespaces.contains(name),
197        }
198    }
199}
200
201fn is_typing_module(module: &str) -> bool {
202    matches!(module, "typing" | "typing_extensions")
203}
204
205fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
206    brokk_bifrost_core::analyzer::common::node_source_text(node, source)
207}
208
209/// Return the name-bearing node of a Python expression using tree-sitter fields.
210pub fn expression_name_node<'tree>(expression: Node<'tree>) -> Option<Node<'tree>> {
211    let mut current = expression;
212    loop {
213        match current.kind() {
214            "identifier" => return Some(current),
215            "attribute" => current = current.child_by_field_name("attribute")?,
216            "call" => current = current.child_by_field_name("function")?,
217            _ => return None,
218        }
219    }
220}
221
222/// Whether `node` is the label of a keyword argument: the `x` in `f(x=1)`.
223///
224/// The label names a parameter or member at the CALLEE, selected by the call's
225/// target, not by anything bound where the label is written. That is why the
226/// occurrence-role adapter in `structural.rs` classifies it `LabelOrKey`, whose
227/// occurrence class is `NonReference`, and it is the rule the reference census
228/// reads to decide that the label is not a forward-reference probe (#2054).
229pub fn python_keyword_argument_label(node: Node<'_>) -> bool {
230    node.kind() == "identifier"
231        && node.parent().is_some_and(|parent| {
232            parent.kind() == "keyword_argument" && parent.child_by_field_name("name") == Some(node)
233        })
234}
235
236/// Return a decorator's callable expression, peeling an optional invocation.
237pub fn decorator_callee<'tree>(decorator: Node<'tree>) -> Option<Node<'tree>> {
238    if decorator.kind() != "decorator" {
239        return None;
240    }
241    let mut expression = decorator.named_child(0)?;
242    while expression.kind() == "call" {
243        expression = expression.child_by_field_name("function")?;
244    }
245    Some(expression)
246}
247
248/// Whether `node` is contained by a parser field that Python evaluates as an
249/// annotation rather than as an ordinary expression.
250pub fn python_node_is_in_annotation(node: Node<'_>) -> bool {
251    let start = node.start_byte();
252    let end = node.end_byte();
253    let mut current = node;
254    while let Some(parent) = current.parent() {
255        let annotation = match parent.kind() {
256            "function_definition" => parent.child_by_field_name("return_type"),
257            "typed_parameter" | "typed_default_parameter" | "assignment" => {
258                parent.child_by_field_name("type")
259            }
260            _ => None,
261        };
262        if let Some(annotation) = annotation
263            && annotation.start_byte() <= start
264            && end <= annotation.end_byte()
265        {
266            return true;
267        }
268        current = parent;
269    }
270    false
271}
272
273/// Parse one exactly mapped deferred annotation and return its identifier ranges.
274pub fn python_deferred_annotation_identifier_ranges(
275    string: Node<'_>,
276    source: &str,
277    cancellation: Option<&CancellationToken>,
278) -> Option<Vec<Range>> {
279    let tree = python_deferred_annotation_tree(string, source, cancellation)?;
280
281    let mut ranges = Vec::new();
282    let mut stack = vec![tree.root_node()];
283    while let Some(current) = stack.pop() {
284        if cancellation.is_some_and(CancellationToken::is_cancelled) {
285            return None;
286        }
287        if current.kind() == "identifier" {
288            ranges.push(Range {
289                start_byte: current.start_byte(),
290                end_byte: current.end_byte(),
291                start_line: current.start_position().row + 1,
292                end_line: current.end_position().row + 1,
293            });
294        }
295        for index in (0..current.named_child_count()).rev() {
296            if let Some(child) = current.named_child(index) {
297                stack.push(child);
298            }
299        }
300    }
301    Some(ranges)
302}
303
304/// Parse one quoted annotation expression while preserving its original source
305/// byte coordinates. Literal string values and arbitrary strings are rejected
306/// by the same structured gate used by inverse membership.
307pub fn python_deferred_annotation_tree(
308    string: Node<'_>,
309    source: &str,
310    cancellation: Option<&CancellationToken>,
311) -> Option<Tree> {
312    if string.kind() != "string"
313        || string
314            .parent()
315            .is_some_and(|parent| parent.kind() == "concatenated_string")
316        || !python_node_is_in_annotation(string)
317        || python_string_is_literal_value(string, source)
318    {
319        return None;
320    }
321
322    let mut content = None;
323    for index in 0..string.named_child_count() {
324        let child = string.named_child(index)?;
325        match child.kind() {
326            "string_start" | "string_end" => {}
327            "string_content" if content.is_none() => content = Some(child),
328            _ => return None,
329        }
330    }
331    let content = content?;
332    let language = tree_sitter_python::LANGUAGE.into();
333    let tree = brokk_bifrost_core::analyzer::common::parse_source_range_with_cancellation(
334        &language,
335        source,
336        content.range(),
337        cancellation,
338    )?;
339    if tree.root_node().has_error() {
340        return None;
341    }
342    Some(tree)
343}
344
345/// Whether `string` is a value argument of `Literal[...]`, rather than a
346/// deferred type expression merely because the whole subscript is an
347/// annotation.
348fn python_string_is_literal_value(string: Node<'_>, source: &str) -> bool {
349    let start = string.start_byte();
350    let end = string.end_byte();
351    let mut current = string;
352    while let Some(parent) = current.parent() {
353        match parent.kind() {
354            "subscript" => {
355                let Some(value) = parent.child_by_field_name("value") else {
356                    return false;
357                };
358                if value.start_byte() <= start && end <= value.end_byte() {
359                    return false;
360                }
361                return python_literal_annotation_base(value, source);
362            }
363            "generic_type" => {
364                let Some(value) = parent.named_child(0) else {
365                    return false;
366                };
367                return python_literal_annotation_base(value, source);
368            }
369            _ => current = parent,
370        }
371    }
372    false
373}
374
375fn python_literal_annotation_base(value: Node<'_>, source: &str) -> bool {
376    match value.kind() {
377        "identifier" => node_text(value, source) == "Literal",
378        "attribute" => {
379            let (Some(object), Some(attribute)) = (
380                value.child_by_field_name("object"),
381                value.child_by_field_name("attribute"),
382            ) else {
383                return false;
384            };
385            object.kind() == "identifier"
386                && matches!(node_text(object, source), "typing" | "typing_extensions")
387                && attribute.kind() == "identifier"
388                && node_text(attribute, source) == "Literal"
389        }
390        "member_type" => {
391            let mut identifiers = Vec::new();
392            let mut stack = vec![value];
393            while let Some(node) = stack.pop() {
394                if node.kind() == "identifier" {
395                    identifiers.push(node_text(node, source));
396                    continue;
397                }
398                for index in (0..node.named_child_count()).rev() {
399                    if let Some(child) = node.named_child(index) {
400                        stack.push(child);
401                    }
402                }
403            }
404            matches!(
405                identifiers.as_slice(),
406                ["typing", "Literal"] | ["typing_extensions", "Literal"]
407            )
408        }
409        _ => false,
410    }
411}