Skip to main content

brokk_bifrost_python/
syntax.rs

1use brokk_bifrost_core::analyzer::Range;
2use brokk_bifrost_core::analyzer::common::node_source_text as node_text;
3use brokk_bifrost_core::cancellation::CancellationToken;
4use brokk_bifrost_core::hash::HashSet;
5use tree_sitter::{Node, Tree};
6
7pub(crate) fn python_parameter_name(node: Node<'_>, source: &str) -> Option<String> {
8    python_parameter_name_bounded(node, source, || true)
9        .expect("unbounded parameter name lookup completes")
10}
11
12pub(crate) fn python_parameter_name_bounded(
13    mut node: Node<'_>,
14    source: &str,
15    mut scope_step: impl FnMut() -> bool,
16) -> Option<Option<String>> {
17    loop {
18        if !scope_step() {
19            return None;
20        }
21        match node.kind() {
22            "identifier" => {
23                let name = node_text(node, source).trim();
24                return Some((!name.is_empty()).then(|| name.to_string()));
25            }
26            "typed_parameter"
27            | "typed_default_parameter"
28            | "default_parameter"
29            | "list_splat_pattern"
30            | "dictionary_splat_pattern" => {
31                let name = if let Some(name) = node.child_by_field_name("name") {
32                    Some(name)
33                } else {
34                    let mut selected = None;
35                    let mut cursor = node.walk();
36                    for child in node.named_children(&mut cursor) {
37                        if !scope_step() {
38                            return None;
39                        }
40                        if child.kind() == "identifier" {
41                            selected = Some(child);
42                            break;
43                        }
44                    }
45                    selected
46                };
47                let Some(name) = name else {
48                    return Some(None);
49                };
50                node = name;
51            }
52            _ => return Some(None),
53        }
54    }
55}
56
57pub fn leftmost_identifier(mut node: Node<'_>) -> Option<Node<'_>> {
58    loop {
59        match node.kind() {
60            "identifier" => return Some(node),
61            "attribute" => node = node.child_by_field_name("object")?,
62            _ => return None,
63        }
64    }
65}
66
67pub(crate) fn attribute_chain<'tree>(node: Node<'tree>) -> Option<(Node<'tree>, Vec<Node<'tree>>)> {
68    let mut attributes = Vec::new();
69    let mut current = node;
70    loop {
71        if current.kind() != "attribute" {
72            return None;
73        }
74        attributes.push(current.child_by_field_name("attribute")?);
75        current = current.child_by_field_name("object")?;
76        if current.kind() == "identifier" {
77            attributes.reverse();
78            return Some((current, attributes));
79        }
80    }
81}
82
83pub fn python_reference_is_deferred_function_body(node: Node<'_>) -> bool {
84    let site_start = node.start_byte();
85    let site_end = node.end_byte();
86    let mut current = node;
87    while let Some(parent) = current.parent() {
88        if matches!(parent.kind(), "function_definition" | "lambda")
89            && parent
90                .child_by_field_name("body")
91                .is_some_and(|body| body.start_byte() <= site_start && site_end <= body.end_byte())
92        {
93            return true;
94        }
95        current = parent;
96    }
97    false
98}
99
100/// The identifier nodes in one static Python value path, from root to leaf.
101///
102/// Dynamic receivers and subscripts have no static path. Keeping the nodes
103/// lets callers combine the parser shape with lexical/import binding facts
104/// without reparsing a dotted source spelling.
105pub fn python_static_attribute_path<'tree>(node: Node<'tree>) -> Option<Vec<Node<'tree>>> {
106    python_static_value_path_with(node, || true, |_| None)
107        .expect("unbounded static path lookup completes")
108}
109
110/// The strict static attribute path proof, charging traversal and reversal.
111/// Parentheses remain outside this API's admitted grammar.
112pub fn python_static_attribute_path_bounded<'tree>(
113    node: Node<'tree>,
114    scope_step: impl FnMut() -> bool,
115) -> Option<Vec<Node<'tree>>> {
116    python_static_value_path_with(node, scope_step, |_| None).flatten()
117}
118
119/// A metered static value path, including transparent parentheses. The outer
120/// `None` means exhaustion; `Some(None)` means the receiver is not static.
121pub fn python_static_value_path_bounded<'tree>(
122    node: Node<'tree>,
123    scope_step: impl FnMut() -> bool,
124) -> Option<Option<Vec<Node<'tree>>>> {
125    python_static_value_path_with(
126        node,
127        scope_step,
128        crate::structural::python_parenthesized_expression_child,
129    )
130}
131
132fn python_static_value_path_with<'tree>(
133    mut node: Node<'tree>,
134    mut scope_step: impl FnMut() -> bool,
135    mut parenthesized_child: impl FnMut(Node<'tree>) -> Option<Node<'tree>>,
136) -> Option<Option<Vec<Node<'tree>>>> {
137    let mut path = Vec::new();
138    loop {
139        if !scope_step() {
140            return None;
141        }
142        match node.kind() {
143            "identifier" => {
144                path.push(node);
145                break;
146            }
147            "attribute" => {
148                let Some(object) = push_static_attribute_segment(node, &mut path) else {
149                    return Some(None);
150                };
151                node = object;
152            }
153            "parenthesized_expression" => {
154                // The shared grammar helper skips comments. Charge all of its
155                // immediate inputs before it inspects them.
156                for _ in brokk_bifrost_core::analyzer::tree_walk::named_children_iter(node) {
157                    if !scope_step() {
158                        return None;
159                    }
160                }
161                let Some(child) = parenthesized_child(node) else {
162                    return Some(None);
163                };
164                node = child;
165            }
166            _ => return Some(None),
167        }
168    }
169    for _ in 0..path.len() {
170        if !scope_step() {
171            return None;
172        }
173    }
174    path.reverse();
175    Some(Some(path))
176}
177
178/// The identifier nodes in one static Python annotation name, root to leaf.
179/// Wrappers and generic arguments are ignored while the named generic origin
180/// remains part of the path.
181pub fn python_static_type_path<'tree>(mut node: Node<'tree>) -> Option<Vec<Node<'tree>>> {
182    let mut path = Vec::new();
183    loop {
184        match node.kind() {
185            "identifier" => {
186                path.push(node);
187                break;
188            }
189            "type" | "generic_type" | "subscript" => node = node.named_child(0)?,
190            "attribute" => {
191                node = push_static_attribute_segment(node, &mut path)?;
192            }
193            "member_type" => {
194                let mut cursor = node.walk();
195                let mut children = node.named_children(&mut cursor);
196                let qualifier = children.next()?;
197                let member = children.next()?;
198                if member.kind() != "identifier" || children.next().is_some() {
199                    return None;
200                }
201                path.push(member);
202                node = qualifier;
203            }
204            _ => return None,
205        }
206    }
207    path.reverse();
208    Some(path)
209}
210
211fn push_static_attribute_segment<'tree>(
212    node: Node<'tree>,
213    path: &mut Vec<Node<'tree>>,
214) -> Option<Node<'tree>> {
215    let attribute = node.child_by_field_name("attribute")?;
216    if attribute.kind() != "identifier" {
217        return None;
218    }
219    path.push(attribute);
220    node.child_by_field_name("object")
221}
222
223/// The text one plain string literal denotes.
224///
225/// Prefixed strings, interpolations, escapes, implicit concatenations, and
226/// malformed shapes return `None`. The returned slice is always the exact
227/// `string_content` AST span, never text recovered by delimiter parsing.
228pub fn python_plain_string_literal<'source>(
229    node: Node<'_>,
230    source: &'source str,
231) -> Option<&'source str> {
232    if node.kind() != "string"
233        || node
234            .parent()
235            .is_some_and(|parent| parent.kind() == "concatenated_string")
236    {
237        return None;
238    }
239    let mut content = None;
240    let mut cursor = node.walk();
241    for child in node.named_children(&mut cursor) {
242        match child.kind() {
243            "string_start" | "string_end" => {
244                let delimiter = child.utf8_text(source.as_bytes()).ok()?;
245                if delimiter
246                    .chars()
247                    .any(|character| character != '"' && character != '\'')
248                {
249                    return None;
250                }
251            }
252            "string_content" if child.named_child_count() == 0 && content.is_none() => {
253                content = Some(child);
254            }
255            _ => return None,
256        }
257    }
258    Some(content.map_or("", |child| {
259        child
260            .utf8_text(source.as_bytes())
261            .expect("a tree-sitter node range is valid UTF-8 source")
262    }))
263}
264
265#[derive(Debug, Default)]
266pub struct PythonOverloadDecoratorBindings {
267    direct: HashSet<String>,
268    namespaces: HashSet<String>,
269}
270
271#[derive(Debug)]
272pub(crate) enum PythonOverloadDecoratorName {
273    Direct(String),
274    Namespace(String),
275}
276
277impl PythonOverloadDecoratorBindings {
278    /// Capture typing bindings from the import interpretation already owned by
279    /// the primary producer. Only module-level leaves reach this collector.
280    pub(crate) fn collect_import(
281        &mut self,
282        import: &brokk_bifrost_core::analyzer::model::ImportInfo,
283    ) {
284        use brokk_bifrost_core::analyzer::model::StructuredImportPathKind;
285        let Some(path) = &import.path else { return };
286        match (path.kind, path.segments.as_slice()) {
287            (Some(StructuredImportPathKind::Namespace), [module]) if is_typing_module(module) => {
288                self.namespaces
289                    .insert(import.alias.as_ref().unwrap_or(module).clone());
290            }
291            (Some(StructuredImportPathKind::ImportFrom), [module, name])
292                if is_typing_module(module) && name == "overload" =>
293            {
294                self.direct
295                    .insert(import.alias.as_ref().unwrap_or(name).clone());
296            }
297            _ => {}
298        }
299    }
300
301    /// Return the exact decorator binding shapes used by one function. The
302    /// names are captured during the primary walk; the import binding set is
303    /// resolved after the walk so a later typing import is handled exactly as
304    /// the historical whole-file predicate did.
305    pub(crate) fn overload_decorator_names(
306        function: Node<'_>,
307        source: &str,
308    ) -> Vec<PythonOverloadDecoratorName> {
309        let Some(parent) = function
310            .parent()
311            .filter(|node| node.kind() == "decorated_definition")
312        else {
313            return Vec::new();
314        };
315        let mut cursor = parent.walk();
316        parent
317            .named_children(&mut cursor)
318            .filter(|child| child.kind() == "decorator")
319            .filter_map(decorator_callee)
320            .filter_map(|callee| match callee.kind() {
321                "identifier" => Some(PythonOverloadDecoratorName::Direct(
322                    node_text(callee, source).trim().to_string(),
323                )),
324                "attribute" => {
325                    let attribute = callee.child_by_field_name("attribute")?;
326                    if node_text(attribute, source).trim() != "overload" {
327                        return None;
328                    }
329                    let object = callee.child_by_field_name("object")?;
330                    (object.kind() == "identifier").then(|| {
331                        PythonOverloadDecoratorName::Namespace(
332                            node_text(object, source).trim().to_string(),
333                        )
334                    })
335                }
336                _ => None,
337            })
338            .collect()
339    }
340
341    pub(crate) fn matches_overload_binding(&self, name: &PythonOverloadDecoratorName) -> bool {
342        match name {
343            PythonOverloadDecoratorName::Direct(name) => self.direct.contains(name),
344            PythonOverloadDecoratorName::Namespace(name) => self.namespaces.contains(name),
345        }
346    }
347}
348
349fn is_typing_module(module: &str) -> bool {
350    matches!(module, "typing" | "typing_extensions")
351}
352
353/// Return the name-bearing node of a Python expression using tree-sitter fields.
354pub fn expression_name_node<'tree>(expression: Node<'tree>) -> Option<Node<'tree>> {
355    let mut current = expression;
356    loop {
357        match current.kind() {
358            "identifier" => return Some(current),
359            "attribute" => current = current.child_by_field_name("attribute")?,
360            "call" => current = current.child_by_field_name("function")?,
361            _ => return None,
362        }
363    }
364}
365
366/// Whether `node` is the label of a keyword argument: the `x` in `f(x=1)`.
367///
368/// The label names a parameter or member at the CALLEE, selected by the call's
369/// target, not by anything bound where the label is written. That is why the
370/// occurrence-role adapter in `structural.rs` classifies it `LabelOrKey`, whose
371/// occurrence class is `NonReference`, and it is the rule the reference census
372/// reads to decide that the label is not a forward-reference probe (#2054).
373pub fn python_keyword_argument_label(node: Node<'_>) -> bool {
374    node.kind() == "identifier"
375        && node.parent().is_some_and(|parent| {
376            parent.kind() == "keyword_argument" && parent.child_by_field_name("name") == Some(node)
377        })
378}
379
380/// Return a decorator's callable expression, peeling an optional invocation.
381pub fn decorator_callee<'tree>(decorator: Node<'tree>) -> Option<Node<'tree>> {
382    if decorator.kind() != "decorator" {
383        return None;
384    }
385    // The grammar can interleave a `line_continuation` extra before the
386    // decorated expression; skip extras so `@\`-continued names resolve.
387    let mut cursor = decorator.walk();
388    let mut expression = decorator
389        .named_children(&mut cursor)
390        .find(|child| !child.is_extra())?;
391    while expression.kind() == "call" {
392        expression = expression.child_by_field_name("function")?;
393    }
394    Some(expression)
395}
396
397/// Whether `node` is contained by a parser field that Python evaluates as an
398/// annotation rather than as an ordinary expression.
399pub fn python_node_is_in_annotation(node: Node<'_>) -> bool {
400    let start = node.start_byte();
401    let end = node.end_byte();
402    let mut current = node;
403    while let Some(parent) = current.parent() {
404        let annotation = match parent.kind() {
405            "function_definition" => parent.child_by_field_name("return_type"),
406            "typed_parameter" | "typed_default_parameter" | "assignment" => {
407                parent.child_by_field_name("type")
408            }
409            _ => None,
410        };
411        if let Some(annotation) = annotation
412            && annotation.start_byte() <= start
413            && end <= annotation.end_byte()
414        {
415            return true;
416        }
417        current = parent;
418    }
419    false
420}
421
422/// Parse one exactly mapped deferred annotation and return its identifier ranges.
423pub fn python_deferred_annotation_identifier_ranges(
424    string: Node<'_>,
425    source: &str,
426    cancellation: Option<&CancellationToken>,
427) -> Option<Vec<Range>> {
428    let tree = python_deferred_annotation_tree(string, source, cancellation)?;
429
430    let mut ranges = Vec::new();
431    let mut stack = vec![tree.root_node()];
432    while let Some(current) = stack.pop() {
433        if cancellation.is_some_and(CancellationToken::is_cancelled) {
434            return None;
435        }
436        if current.kind() == "identifier" {
437            ranges.push(Range {
438                start_byte: current.start_byte(),
439                end_byte: current.end_byte(),
440                start_line: current.start_position().row + 1,
441                end_line: current.end_position().row + 1,
442            });
443        }
444        for index in (0..current.named_child_count()).rev() {
445            if let Some(child) = current.named_child(index) {
446                stack.push(child);
447            }
448        }
449    }
450    Some(ranges)
451}
452
453/// Parse one quoted annotation expression while preserving its original source
454/// byte coordinates. Literal string values and arbitrary strings are rejected
455/// by the same structured gate used by inverse membership.
456pub fn python_deferred_annotation_tree(
457    string: Node<'_>,
458    source: &str,
459    cancellation: Option<&CancellationToken>,
460) -> Option<Tree> {
461    if string.kind() != "string"
462        || string
463            .parent()
464            .is_some_and(|parent| parent.kind() == "concatenated_string")
465        || !python_node_is_in_annotation(string)
466        || python_string_is_literal_value(string, source)
467    {
468        return None;
469    }
470
471    let mut content = None;
472    for index in 0..string.named_child_count() {
473        let child = string.named_child(index)?;
474        match child.kind() {
475            "string_start" | "string_end" => {}
476            "string_content" if content.is_none() => content = Some(child),
477            _ => return None,
478        }
479    }
480    let content = content?;
481    let language = tree_sitter_python::LANGUAGE.into();
482    let tree = brokk_bifrost_core::analyzer::common::parse_source_range_with_cancellation(
483        &language,
484        source,
485        content.range(),
486        cancellation,
487    )?;
488    if tree.root_node().has_error() {
489        return None;
490    }
491    Some(tree)
492}
493
494/// Whether `string` is a value argument of `Literal[...]`, rather than a
495/// deferred type expression merely because the whole subscript is an
496/// annotation.
497fn python_string_is_literal_value(string: Node<'_>, source: &str) -> bool {
498    let start = string.start_byte();
499    let end = string.end_byte();
500    let mut current = string;
501    while let Some(parent) = current.parent() {
502        match parent.kind() {
503            "subscript" => {
504                let Some(value) = parent.child_by_field_name("value") else {
505                    return false;
506                };
507                if value.start_byte() <= start && end <= value.end_byte() {
508                    return false;
509                }
510                return python_literal_annotation_base(value, source);
511            }
512            "generic_type" => {
513                let Some(value) = parent.named_child(0) else {
514                    return false;
515                };
516                return python_literal_annotation_base(value, source);
517            }
518            _ => current = parent,
519        }
520    }
521    false
522}
523
524fn python_literal_annotation_base(value: Node<'_>, source: &str) -> bool {
525    match value.kind() {
526        "identifier" => node_text(value, source) == "Literal",
527        "attribute" => {
528            let (Some(object), Some(attribute)) = (
529                value.child_by_field_name("object"),
530                value.child_by_field_name("attribute"),
531            ) else {
532                return false;
533            };
534            object.kind() == "identifier"
535                && matches!(node_text(object, source), "typing" | "typing_extensions")
536                && attribute.kind() == "identifier"
537                && node_text(attribute, source) == "Literal"
538        }
539        "member_type" => {
540            let mut identifiers = Vec::new();
541            let mut stack = vec![value];
542            while let Some(node) = stack.pop() {
543                if node.kind() == "identifier" {
544                    identifiers.push(node_text(node, source));
545                    continue;
546                }
547                for index in (0..node.named_child_count()).rev() {
548                    if let Some(child) = node.named_child(index) {
549                        stack.push(child);
550                    }
551                }
552            }
553            matches!(
554                identifiers.as_slice(),
555                ["typing", "Literal"] | ["typing_extensions", "Literal"]
556            )
557        }
558        _ => false,
559    }
560}
561
562#[cfg(test)]
563mod tests {
564    use brokk_bifrost_test_macros::hardwon;
565
566    #[test]
567    #[hardwon("#5044: commented parentheses must preserve import identity and budget exhaustion")]
568    fn static_paths_keep_parentheses_comments_and_exhaustion_distinct() {
569        let source = "(  # receiver\n    module).function\n";
570        let tree = crate::declarations::parse_python_tree(source).unwrap();
571        let expression = tree
572            .root_node()
573            .named_child(0)
574            .unwrap()
575            .named_child(0)
576            .unwrap();
577        // Preserve the producer-facing helper's existing admission contract.
578        assert!(super::python_static_attribute_path(expression).is_none());
579        assert!(super::python_static_value_path_bounded(expression, || false).is_none());
580        let path = super::python_static_value_path_bounded(expression, || true)
581            .unwrap()
582            .unwrap();
583        assert_eq!(
584            path.iter()
585                .map(|node| node.utf8_text(source.as_bytes()).unwrap())
586                .collect::<Vec<_>>(),
587            ["module", "function"]
588        );
589        let dynamic = crate::declarations::parse_python_tree("factory().function\n").unwrap();
590        let expression = dynamic
591            .root_node()
592            .named_child(0)
593            .unwrap()
594            .named_child(0)
595            .unwrap();
596        assert!(matches!(
597            super::python_static_value_path_bounded(expression, || true),
598            Some(None)
599        ));
600    }
601}