Skip to main content

brokk_bifrost_python/
syntax.rs

1use crate::bindings::python_is_type_parameter_binder;
2use brokk_bifrost_core::analyzer::Range;
3use brokk_bifrost_core::analyzer::common::node_source_text as node_text;
4use brokk_bifrost_core::cancellation::CancellationToken;
5use brokk_bifrost_core::hash::HashSet;
6use tree_sitter::{Node, Tree};
7
8/// A decoded module/member route, with Python identifier-valued segments.
9/// This classifies an API string value rather than parsing source syntax.
10pub(crate) fn is_python_member_path(value: &str) -> bool {
11    static IDENTIFIER: std::sync::LazyLock<::regex::Regex> = std::sync::LazyLock::new(|| {
12        ::regex::Regex::new(r"\A[_\p{XID_Start}]\p{XID_Continue}*\z")
13            .expect("Python identifier value regex")
14    });
15    value.contains('.') && value.split('.').all(|part| IDENTIFIER.is_match(part))
16}
17
18/// The lexical class that owns a function, including definitions in compound
19/// class-body statements. Compound suites do not introduce Python scopes.
20pub(crate) fn python_method_owner(definition: Node<'_>) -> Option<Node<'_>> {
21    debug_assert_eq!(definition.kind(), "function_definition");
22    let mut current = definition;
23    while let Some(parent) = current.parent() {
24        match parent.kind() {
25            "class_definition" => {
26                return (parent.child_by_field_name("body") == Some(current)).then_some(parent);
27            }
28            "function_definition" | "lambda" | "module" => return None,
29            _ => current = parent,
30        }
31    }
32    None
33}
34
35/// The operands of a Python conditional expression (`value if test else
36/// alternative`).
37///
38/// The grammar leaves `conditional_expression` without fields, so its operands
39/// can only be selected positionally. Skipping extras is required: a backslash
40/// line continuation before `else` otherwise appears as a fourth named child
41/// and trips the operand-count check (#4797).
42pub fn python_conditional_expression_operands(node: Node<'_>) -> Option<[Node<'_>; 3]> {
43    assert_eq!(node.kind(), "conditional_expression");
44    let mut cursor = node.walk();
45    let operands: Vec<Node<'_>> = node
46        .named_children(&mut cursor)
47        .filter(|child| !child.is_extra())
48        .collect();
49    operands.try_into().ok()
50}
51
52pub(crate) fn python_parameter_name(node: Node<'_>, source: &str) -> Option<String> {
53    python_parameter_name_bounded(node, source, || true)
54        .expect("unbounded parameter name lookup completes")
55}
56
57pub(crate) fn python_parameter_name_bounded(
58    node: Node<'_>,
59    source: &str,
60    scope_step: impl FnMut() -> bool,
61) -> Option<Option<String>> {
62    python_parameter_node_bounded(node, scope_step).map(|node| {
63        node.and_then(|node| {
64            let name = node_text(node, source).trim();
65            (!name.is_empty()).then(|| name.to_owned())
66        })
67    })
68}
69
70/// The exact binder token, shared by signature and native lexical production.
71pub(crate) fn python_parameter_node(node: Node<'_>) -> Option<Node<'_>> {
72    python_parameter_node_bounded(node, || true).expect("unbounded parameter token lookup")
73}
74
75fn python_parameter_node_bounded(
76    mut node: Node<'_>,
77    mut scope_step: impl FnMut() -> bool,
78) -> Option<Option<Node<'_>>> {
79    loop {
80        if !scope_step() {
81            return None;
82        }
83        match node.kind() {
84            "identifier" => return Some((!node.is_missing()).then_some(node)),
85            "typed_parameter"
86            | "typed_default_parameter"
87            | "default_parameter"
88            | "list_splat_pattern"
89            | "dictionary_splat_pattern" => {
90                if let Some(name) = node.child_by_field_name("name") {
91                    node = name;
92                    continue;
93                }
94                let mut selected = None;
95                let mut cursor = node.walk();
96                for child in node.named_children(&mut cursor) {
97                    if !scope_step() {
98                        return None;
99                    }
100                    if matches!(
101                        child.kind(),
102                        "identifier" | "list_splat_pattern" | "dictionary_splat_pattern"
103                    ) {
104                        selected = Some(child);
105                        break;
106                    }
107                }
108                let Some(name) = selected else {
109                    return Some(None);
110                };
111                node = name;
112            }
113            _ => return Some(None),
114        }
115    }
116}
117
118pub fn leftmost_identifier(mut node: Node<'_>) -> Option<Node<'_>> {
119    loop {
120        match node.kind() {
121            "identifier" => return Some(node),
122            "attribute" => node = node.child_by_field_name("object")?,
123            _ => return None,
124        }
125    }
126}
127
128pub(crate) fn attribute_chain<'tree>(node: Node<'tree>) -> Option<(Node<'tree>, Vec<Node<'tree>>)> {
129    let mut attributes = Vec::new();
130    let mut current = node;
131    loop {
132        if current.kind() != "attribute" {
133            return None;
134        }
135        attributes.push(current.child_by_field_name("attribute")?);
136        current = current.child_by_field_name("object")?;
137        if current.kind() == "identifier" {
138            attributes.reverse();
139            return Some((current, attributes));
140        }
141    }
142}
143
144pub fn python_reference_is_deferred_function_body(node: Node<'_>) -> bool {
145    python_reference_is_deferred_function_body_bounded(node, || true)
146        .expect("unbounded deferred-context lookup completes")
147}
148
149/// Body containment under the caller's sticky traversal budget.
150pub fn python_reference_is_deferred_function_body_bounded(
151    node: Node<'_>,
152    mut scope_step: impl FnMut() -> bool,
153) -> Option<bool> {
154    let site_start = node.start_byte();
155    let site_end = node.end_byte();
156    let mut current = node;
157    while let Some(parent) = current.parent() {
158        if !scope_step() {
159            return None;
160        }
161        if matches!(parent.kind(), "function_definition" | "lambda")
162            && parent
163                .child_by_field_name("body")
164                .is_some_and(|body| body.start_byte() <= site_start && site_end <= body.end_byte())
165        {
166            return Some(true);
167        }
168        current = parent;
169    }
170    Some(false)
171}
172
173/// The identifier nodes in one static Python value path, from root to leaf.
174///
175/// Dynamic receivers and subscripts have no static path. Keeping the nodes
176/// lets callers combine the parser shape with lexical/import binding facts
177/// without reparsing a dotted source spelling.
178pub fn python_static_attribute_path<'tree>(node: Node<'tree>) -> Option<Vec<Node<'tree>>> {
179    python_static_value_path_with(node, || true, |_| None)
180        .expect("unbounded static path lookup completes")
181}
182
183/// The strict static attribute path proof, charging traversal and reversal.
184/// Parentheses remain outside this API's admitted grammar.
185pub fn python_static_attribute_path_bounded<'tree>(
186    node: Node<'tree>,
187    scope_step: impl FnMut() -> bool,
188) -> Option<Vec<Node<'tree>>> {
189    python_static_value_path_with(node, scope_step, |_| None).flatten()
190}
191
192/// A metered static value path, including transparent parentheses. The outer
193/// `None` means exhaustion; `Some(None)` means the receiver is not static.
194pub fn python_static_value_path_bounded<'tree>(
195    node: Node<'tree>,
196    scope_step: impl FnMut() -> bool,
197) -> Option<Option<Vec<Node<'tree>>>> {
198    python_static_value_path_with(
199        node,
200        scope_step,
201        crate::structural::python_parenthesized_expression_child,
202    )
203}
204
205fn python_static_value_path_with<'tree>(
206    mut node: Node<'tree>,
207    mut scope_step: impl FnMut() -> bool,
208    mut parenthesized_child: impl FnMut(Node<'tree>) -> Option<Node<'tree>>,
209) -> Option<Option<Vec<Node<'tree>>>> {
210    let mut path = Vec::new();
211    loop {
212        if !scope_step() {
213            return None;
214        }
215        match node.kind() {
216            "identifier" => {
217                path.push(node);
218                break;
219            }
220            "attribute" => {
221                let Some(object) = push_static_attribute_segment(node, &mut path) else {
222                    return Some(None);
223                };
224                node = object;
225            }
226            "parenthesized_expression" => {
227                // The shared grammar helper skips comments. Charge all of its
228                // immediate inputs before it inspects them.
229                for _ in brokk_bifrost_core::analyzer::tree_walk::named_children_iter(node) {
230                    if !scope_step() {
231                        return None;
232                    }
233                }
234                let Some(child) = parenthesized_child(node) else {
235                    return Some(None);
236                };
237                node = child;
238            }
239            _ => return Some(None),
240        }
241    }
242    for _ in 0..path.len() {
243        if !scope_step() {
244            return None;
245        }
246    }
247    path.reverse();
248    Some(Some(path))
249}
250
251/// The identifier nodes in one static Python annotation name, root to leaf.
252/// Wrappers and generic arguments are ignored while the named generic origin
253/// remains part of the path.
254pub fn python_static_type_path<'tree>(mut node: Node<'tree>) -> Option<Vec<Node<'tree>>> {
255    let mut path = Vec::new();
256    loop {
257        match node.kind() {
258            "identifier" => {
259                path.push(node);
260                break;
261            }
262            "type" | "generic_type" | "subscript" => node = node.named_child(0)?,
263            "attribute" => {
264                node = push_static_attribute_segment(node, &mut path)?;
265            }
266            "member_type" => {
267                let mut cursor = node.walk();
268                let mut children = node.named_children(&mut cursor);
269                let qualifier = children.next()?;
270                let member = children.next()?;
271                if member.kind() != "identifier" || children.next().is_some() {
272                    return None;
273                }
274                path.push(member);
275                node = qualifier;
276            }
277            _ => return None,
278        }
279    }
280    path.reverse();
281    Some(path)
282}
283
284fn push_static_attribute_segment<'tree>(
285    node: Node<'tree>,
286    path: &mut Vec<Node<'tree>>,
287) -> Option<Node<'tree>> {
288    let attribute = node.child_by_field_name("attribute")?;
289    if attribute.kind() != "identifier" {
290        return None;
291    }
292    path.push(attribute);
293    node.child_by_field_name("object")
294}
295
296/// The text one plain string literal denotes.
297///
298/// Prefixed strings, interpolations, escapes, implicit concatenations, and
299/// malformed shapes return `None`. The returned slice is always the exact
300/// `string_content` AST span, never text recovered by delimiter parsing.
301pub fn python_plain_string_literal<'source>(
302    node: Node<'_>,
303    source: &'source str,
304) -> Option<&'source str> {
305    if node.kind() != "string"
306        || node
307            .parent()
308            .is_some_and(|parent| parent.kind() == "concatenated_string")
309    {
310        return None;
311    }
312    let mut content = None;
313    let mut cursor = node.walk();
314    for child in node.named_children(&mut cursor) {
315        match child.kind() {
316            "string_start" | "string_end" => {
317                let delimiter = child.utf8_text(source.as_bytes()).ok()?;
318                if delimiter
319                    .chars()
320                    .any(|character| character != '"' && character != '\'')
321                {
322                    return None;
323                }
324            }
325            "string_content" if child.named_child_count() == 0 && content.is_none() => {
326                content = Some(child);
327            }
328            _ => return None,
329        }
330    }
331    Some(content.map_or("", |child| {
332        child
333            .utf8_text(source.as_bytes())
334            .expect("a tree-sitter node range is valid UTF-8 source")
335    }))
336}
337
338#[derive(Debug, Default)]
339pub struct PythonOverloadDecoratorBindings {
340    direct: HashSet<String>,
341    namespaces: HashSet<String>,
342}
343
344#[derive(Debug)]
345pub(crate) enum PythonOverloadDecoratorName {
346    Direct(String),
347    Namespace(String),
348}
349
350impl PythonOverloadDecoratorBindings {
351    /// Capture typing bindings from the import interpretation already owned by
352    /// the primary producer. Only module-level leaves reach this collector.
353    pub(crate) fn collect_import(
354        &mut self,
355        import: &brokk_bifrost_core::analyzer::model::ImportInfo,
356    ) {
357        use brokk_bifrost_core::analyzer::model::StructuredImportPathKind;
358        let Some(path) = &import.path else { return };
359        match (path.kind, path.segments.as_slice()) {
360            (Some(StructuredImportPathKind::Namespace), [module]) if is_typing_module(module) => {
361                self.namespaces
362                    .insert(import.alias.as_ref().unwrap_or(module).clone());
363            }
364            (Some(StructuredImportPathKind::ImportFrom), [module, name])
365                if is_typing_module(module) && name == "overload" =>
366            {
367                self.direct
368                    .insert(import.alias.as_ref().unwrap_or(name).clone());
369            }
370            _ => {}
371        }
372    }
373
374    /// Return the exact decorator binding shapes used by one function. The
375    /// names are captured during the primary walk; the import binding set is
376    /// resolved after the walk so a later typing import is handled exactly as
377    /// the historical whole-file predicate did.
378    pub(crate) fn overload_decorator_names(
379        function: Node<'_>,
380        source: &str,
381    ) -> Vec<PythonOverloadDecoratorName> {
382        let Some(parent) = function
383            .parent()
384            .filter(|node| node.kind() == "decorated_definition")
385        else {
386            return Vec::new();
387        };
388        let mut cursor = parent.walk();
389        parent
390            .named_children(&mut cursor)
391            .filter(|child| child.kind() == "decorator")
392            .filter_map(decorator_callee)
393            .filter_map(|callee| match callee.kind() {
394                "identifier" => Some(PythonOverloadDecoratorName::Direct(
395                    node_text(callee, source).trim().to_string(),
396                )),
397                "attribute" => {
398                    let attribute = callee.child_by_field_name("attribute")?;
399                    if node_text(attribute, source).trim() != "overload" {
400                        return None;
401                    }
402                    let object = callee.child_by_field_name("object")?;
403                    (object.kind() == "identifier").then(|| {
404                        PythonOverloadDecoratorName::Namespace(
405                            node_text(object, source).trim().to_string(),
406                        )
407                    })
408                }
409                _ => None,
410            })
411            .collect()
412    }
413
414    pub(crate) fn matches_overload_binding(&self, name: &PythonOverloadDecoratorName) -> bool {
415        match name {
416            PythonOverloadDecoratorName::Direct(name) => self.direct.contains(name),
417            PythonOverloadDecoratorName::Namespace(name) => self.namespaces.contains(name),
418        }
419    }
420}
421
422fn is_typing_module(module: &str) -> bool {
423    matches!(module, "typing" | "typing_extensions")
424}
425
426/// Return the name-bearing node of a Python expression using tree-sitter fields.
427pub fn expression_name_node<'tree>(expression: Node<'tree>) -> Option<Node<'tree>> {
428    let mut current = expression;
429    loop {
430        match current.kind() {
431            "identifier" => return Some(current),
432            "attribute" => current = current.child_by_field_name("attribute")?,
433            "call" => current = current.child_by_field_name("function")?,
434            _ => return None,
435        }
436    }
437}
438
439/// Whether `node` is the label of a keyword argument: the `x` in `f(x=1)`.
440///
441/// The label names a parameter or member at the CALLEE, selected by the call's
442/// target, not by anything bound where the label is written. That is why the
443/// occurrence-role adapter in `structural.rs` classifies it `LabelOrKey`, whose
444/// occurrence class is `NonReference`, and it is the rule the reference census
445/// reads to decide that the label is not a forward-reference probe (#2054).
446pub fn python_keyword_argument_label(node: Node<'_>) -> bool {
447    node.kind() == "identifier"
448        && node.parent().is_some_and(|parent| {
449            parent.kind() == "keyword_argument" && parent.child_by_field_name("name") == Some(node)
450        })
451}
452
453/// Return a decorator's callable expression, peeling an optional invocation.
454pub fn decorator_callee<'tree>(decorator: Node<'tree>) -> Option<Node<'tree>> {
455    if decorator.kind() != "decorator" {
456        return None;
457    }
458    // The grammar can interleave a `line_continuation` extra before the
459    // decorated expression; skip extras so `@\`-continued names resolve.
460    let mut cursor = decorator.walk();
461    let mut expression = decorator
462        .named_children(&mut cursor)
463        .find(|child| !child.is_extra())?;
464    while expression.kind() == "call" {
465        expression = expression.child_by_field_name("function")?;
466    }
467    Some(expression)
468}
469
470/// Whether a named node can carry a Python source reference at the selected range.
471/// Soft keywords used as names parse as `keyword_identifier`, and deferred
472/// annotation strings can name a reference even without an identifier node.
473pub fn python_node_can_be_reference(node: Node<'_>) -> bool {
474    matches!(node.kind(), "identifier" | "keyword_identifier")
475        || (node.kind() == "string_content" && python_node_is_in_annotation(node))
476}
477
478/// Whether `node` is contained by a parser field that Python evaluates as an
479/// annotation rather than as an ordinary expression.
480pub fn python_node_is_in_annotation(node: Node<'_>) -> bool {
481    let start = node.start_byte();
482    let end = node.end_byte();
483    let mut current = node;
484    while let Some(parent) = current.parent() {
485        let annotation = match parent.kind() {
486            "function_definition" => parent.child_by_field_name("return_type"),
487            "typed_parameter" | "typed_default_parameter" | "assignment" => {
488                parent.child_by_field_name("type")
489            }
490            _ => None,
491        };
492        if let Some(annotation) = annotation
493            && annotation.start_byte() <= start
494            && end <= annotation.end_byte()
495        {
496            return true;
497        }
498        current = parent;
499    }
500    false
501}
502
503/// Parse one exactly mapped deferred annotation and return its identifier ranges.
504pub fn python_deferred_annotation_identifier_ranges(
505    string: Node<'_>,
506    source: &str,
507    cancellation: Option<&CancellationToken>,
508) -> Option<Vec<Range>> {
509    let tree = python_deferred_annotation_tree(string, source, cancellation)?;
510
511    let mut ranges = Vec::new();
512    let mut stack = vec![tree.root_node()];
513    while let Some(current) = stack.pop() {
514        if cancellation.is_some_and(CancellationToken::is_cancelled) {
515            return None;
516        }
517        if current.kind() == "identifier" {
518            ranges.push(Range {
519                start_byte: current.start_byte(),
520                end_byte: current.end_byte(),
521                start_line: current.start_position().row + 1,
522                end_line: current.end_position().row + 1,
523            });
524        }
525        for index in (0..current.named_child_count()).rev() {
526            if let Some(child) = current.named_child(index) {
527                stack.push(child);
528            }
529        }
530    }
531    Some(ranges)
532}
533
534/// Parse one quoted annotation expression while preserving its original source
535/// byte coordinates. Literal string values and arbitrary strings are rejected
536/// by the same structured gate used by inverse membership.
537pub fn python_deferred_annotation_tree(
538    string: Node<'_>,
539    source: &str,
540    cancellation: Option<&CancellationToken>,
541) -> Option<Tree> {
542    if string.kind() != "string"
543        || string
544            .parent()
545            .is_some_and(|parent| parent.kind() == "concatenated_string")
546        || !python_node_is_in_annotation(string)
547        || python_string_is_literal_value(string, source)
548    {
549        return None;
550    }
551
552    let mut content = None;
553    for index in 0..string.named_child_count() {
554        let child = string.named_child(index)?;
555        match child.kind() {
556            "string_start" | "string_end" => {}
557            "string_content" if content.is_none() => content = Some(child),
558            _ => return None,
559        }
560    }
561    let content = content?;
562    let language = tree_sitter_python::LANGUAGE.into();
563    let tree = brokk_bifrost_core::analyzer::common::parse_source_range_with_cancellation(
564        &language,
565        source,
566        content.range(),
567        cancellation,
568    )?;
569    if tree.root_node().has_error() {
570        return None;
571    }
572    Some(tree)
573}
574
575/// Whether `string` is a value argument of `Literal[...]`, rather than a
576/// deferred type expression merely because the whole subscript is an
577/// annotation.
578fn python_string_is_literal_value(string: Node<'_>, source: &str) -> bool {
579    let start = string.start_byte();
580    let end = string.end_byte();
581    let mut current = string;
582    while let Some(parent) = current.parent() {
583        match parent.kind() {
584            "subscript" => {
585                let Some(value) = parent.child_by_field_name("value") else {
586                    return false;
587                };
588                if value.start_byte() <= start && end <= value.end_byte() {
589                    return false;
590                }
591                return python_literal_annotation_base(value, source);
592            }
593            "generic_type" => {
594                let Some(value) = parent.named_child(0) else {
595                    return false;
596                };
597                return python_literal_annotation_base(value, source);
598            }
599            _ => current = parent,
600        }
601    }
602    false
603}
604
605fn python_literal_annotation_base(value: Node<'_>, source: &str) -> bool {
606    match value.kind() {
607        "identifier" => node_text(value, source) == "Literal",
608        "attribute" => {
609            let (Some(object), Some(attribute)) = (
610                value.child_by_field_name("object"),
611                value.child_by_field_name("attribute"),
612            ) else {
613                return false;
614            };
615            object.kind() == "identifier"
616                && matches!(node_text(object, source), "typing" | "typing_extensions")
617                && attribute.kind() == "identifier"
618                && node_text(attribute, source) == "Literal"
619        }
620        "member_type" => {
621            let mut identifiers = Vec::new();
622            let mut stack = vec![value];
623            while let Some(node) = stack.pop() {
624                if node.kind() == "identifier" {
625                    identifiers.push(node_text(node, source));
626                    continue;
627                }
628                for index in (0..node.named_child_count()).rev() {
629                    if let Some(child) = node.named_child(index) {
630                        stack.push(child);
631                    }
632                }
633            }
634            matches!(
635                identifiers.as_slice(),
636                ["typing", "Literal"] | ["typing_extensions", "Literal"]
637            )
638        }
639        _ => false,
640    }
641}
642
643/// A structurally selected subtree mentioning the module export identifier.
644/// Used by both legacy recovery and native export-operation classification.
645pub(crate) fn python_contains_all_identifier(node: Node<'_>, source: &str) -> bool {
646    use brokk_bifrost_core::analyzer::tree_walk::{WalkControl, walk_named_tree_preorder};
647    let mut found = false;
648    walk_named_tree_preorder(node, true, |node| {
649        if node.kind() == "identifier" && &source[node.byte_range()] == "__all__" {
650            found = true;
651            WalkControl::Break
652        } else {
653            WalkControl::Continue
654        }
655    });
656    found
657}
658
659pub fn is_declaration_identifier(node: Node<'_>) -> bool {
660    let contains = |container: Node<'_>| {
661        container.start_byte() <= node.start_byte() && node.end_byte() <= container.end_byte()
662    };
663    let mut current = node;
664    while let Some(parent) = current.parent() {
665        match parent.kind() {
666            "class_definition" | "function_definition" => {
667                return parent
668                    .child_by_field_name("name")
669                    .is_some_and(|name| name.id() == node.id())
670                    || python_is_type_parameter_binder(node);
671            }
672            "parameters"
673            | "lambda_parameters"
674            | "list_splat_pattern"
675            | "dictionary_splat_pattern" => return true,
676            "default_parameter" | "typed_parameter" | "typed_default_parameter" => {
677                return parent.child_by_field_name("name").is_some_and(contains);
678            }
679            "assignment" | "augmented_assignment" | "for_statement" | "for_in_clause" => {
680                return parent.child_by_field_name("left").is_some_and(contains);
681            }
682            "named_expression" => {
683                return parent.child_by_field_name("name").is_some_and(contains);
684            }
685            "aliased_import" | "import_from_statement" | "import_statement" => return true,
686            "pattern_list"
687            | "tuple_pattern"
688            | "list_pattern"
689            | "parenthesized_expression"
690            | "type"
691            | "type_parameter"
692            | "constrained_type" => current = parent,
693            _ => return false,
694        }
695    }
696    false
697}
698
699/// Whether an attribute call uses the conventional Python receiver name.
700pub fn python_attribute_callee_uses_self_or_cls_bounded(source: &str, callee: Node<'_>) -> bool {
701    let Some(object) = callee.child_by_field_name("object") else {
702        return false;
703    };
704    object.kind() == "identifier" && matches!(node_text(object, source), "self" | "cls")
705}
706
707/// Whether an attribute callee names a nested class in its enclosing class.
708pub fn python_attribute_callee_names_enclosing_nested_class_bounded(
709    source: &str,
710    callee: Node<'_>,
711) -> bool {
712    let Some(attribute) = callee.child_by_field_name("attribute") else {
713        return false;
714    };
715    let name = node_text(attribute, source);
716    let mut ancestor = callee.parent();
717    while let Some(node) = ancestor {
718        if node.kind() == "class_definition" {
719            let Some(body) = node.child_by_field_name("body") else {
720                return false;
721            };
722            let mut cursor = body.walk();
723            return body.named_children(&mut cursor).any(|member| {
724                member.kind() == "class_definition"
725                    && member
726                        .child_by_field_name("name")
727                        .is_some_and(|class_name| node_text(class_name, source) == name)
728            });
729        }
730        ancestor = node.parent();
731    }
732    false
733}
734
735/// Whether an attribute expression reaches a nested class of the enclosing
736/// class through `self` or `cls`. Instance writes or subclass overrides can
737/// shadow such a class, so no single class answers for it.
738pub fn python_attribute_names_nested_class_through_current_receiver_bounded(
739    source: &str,
740    expression: Node<'_>,
741) -> bool {
742    expression.kind() == "attribute"
743        && python_attribute_callee_uses_self_or_cls_bounded(source, expression)
744        && python_attribute_callee_names_enclosing_nested_class_bounded(source, expression)
745}
746
747#[cfg(test)]
748mod tests {
749    use brokk_bifrost_test_macros::hardwon;
750
751    #[test]
752    fn qs6_python_syntax_field_boundary() {
753        use super::*;
754        for (source, expected) in [
755            ("sys.argv[1]", Some("sys.argv")),
756            ("obj.member[key]", Some("obj.member")),
757            ("obj.nested.member[key]", None),
758            ("values[key]", None),
759            ("obj.member", None),
760        ] {
761            let tree = crate::declarations::parse_python_tree(source).unwrap();
762            assert!(!tree.root_node().has_error());
763            let expression = tree
764                .root_node()
765                .named_child(0)
766                .unwrap()
767                .named_child(0)
768                .unwrap();
769            let seed = crate::runtime_values::python_runtime_keyed_access_seed(expression);
770            assert_eq!(seed.map(|node| &source[node.byte_range()]), expected);
771            if let Some(seed) = seed {
772                assert_eq!(seed.kind(), "attribute");
773                assert_eq!(
774                    expression.child_by_field_name("value").unwrap().id(),
775                    seed.id()
776                );
777                assert_eq!(seed.child_by_field_name("object").unwrap().start_byte(), 0);
778                assert_eq!(seed.start_byte(), 0);
779                assert_eq!(seed.end_byte(), expected.unwrap().len());
780            }
781        }
782        for (annotation, names) in [
783            ("pkg.Type", vec!["pkg", "Type"]),
784            ("pkg.Outer.Inner[int]", vec!["pkg", "Outer", "Inner"]),
785            ("Outer[T].Inner", vec!["Outer", "Inner"]),
786        ] {
787            let source = format!("def f() -> {annotation}: pass\n");
788            let tree = crate::declarations::parse_python_tree(&source).unwrap();
789            assert!(!tree.root_node().has_error());
790            let annotation_node = tree
791                .root_node()
792                .named_child(0)
793                .unwrap()
794                .child_by_field_name("return_type")
795                .unwrap();
796            let path = python_static_type_path(annotation_node).unwrap();
797            assert_eq!(
798                path.iter()
799                    .map(|node| &source[node.byte_range()])
800                    .collect::<Vec<_>>(),
801                names
802            );
803            for node in path {
804                assert_eq!(node.kind(), "identifier");
805                assert!(node.start_byte() >= annotation_node.start_byte());
806                assert!(node.end_byte() <= annotation_node.end_byte());
807            }
808        }
809        let source = "@factory()()\ndef f(): pass\n";
810        let tree = crate::declarations::parse_python_tree(source).unwrap();
811        let decorator = tree
812            .root_node()
813            .named_child(0)
814            .unwrap()
815            .named_child(0)
816            .unwrap();
817        assert_eq!(decorator.kind(), "decorator");
818        let call = decorator.named_child(0).unwrap();
819        assert_eq!(call.kind(), "call");
820        assert_eq!(call.child_by_field_name("function").unwrap().kind(), "call");
821        let callee = decorator_callee(decorator).unwrap();
822        assert_eq!(callee.kind(), "identifier");
823        assert_eq!(callee.byte_range(), 1..8);
824        assert_eq!(&source[callee.byte_range()], "factory");
825    }
826
827    #[test]
828    fn qs6_deferred_annotation_live_token_ranges() {
829        use super::*;
830        let source = "# \u{e9}\ndef f(x: \"pkg.Type[Other]\") -> \"Result\": pass\nordinary = \"text\"\ny: Literal[\"value\"]\n";
831        let tree = crate::declarations::parse_python_tree(source).unwrap();
832        assert!(!tree.root_node().has_error());
833        let fresh = CancellationToken::new();
834        let cancelled = CancellationToken::new();
835        cancelled.cancel();
836        let mut pending = vec![tree.root_node()];
837        let mut strings = Vec::new();
838        while let Some(node) = pending.pop() {
839            if node.kind() == "string" {
840                strings.push(node);
841            }
842            for index in (0..node.named_child_count()).rev() {
843                pending.push(node.named_child(index).unwrap());
844            }
845        }
846        assert_eq!(strings.len(), 4);
847        let mut names = Vec::new();
848        for string in strings {
849            let no_token = python_deferred_annotation_identifier_ranges(string, source, None);
850            assert_eq!(
851                python_deferred_annotation_identifier_ranges(string, source, Some(&fresh)),
852                no_token
853            );
854            assert!(
855                python_deferred_annotation_identifier_ranges(string, source, Some(&cancelled))
856                    .is_none()
857            );
858            if let Some(ranges) = no_token {
859                for range in ranges {
860                    assert!(range.start_byte > string.start_byte());
861                    assert!(range.end_byte < string.end_byte());
862                    assert_eq!(range.start_line, 2);
863                    assert_eq!(range.end_line, 2);
864                    names.push(&source[range.start_byte..range.end_byte]);
865                }
866            }
867        }
868        assert_eq!(names, ["pkg", "Type", "Other", "Result"]);
869    }
870
871    #[test]
872    #[hardwon("#5044: commented parentheses must preserve import identity and budget exhaustion")]
873    fn static_paths_keep_parentheses_comments_and_exhaustion_distinct() {
874        let source = "(  # receiver\n    module).function\n";
875        let tree = crate::declarations::parse_python_tree(source).unwrap();
876        let expression = tree
877            .root_node()
878            .named_child(0)
879            .unwrap()
880            .named_child(0)
881            .unwrap();
882        // Preserve the producer-facing helper's existing admission contract.
883        assert!(super::python_static_attribute_path(expression).is_none());
884        assert!(super::python_static_value_path_bounded(expression, || false).is_none());
885        let path = super::python_static_value_path_bounded(expression, || true)
886            .unwrap()
887            .unwrap();
888        assert_eq!(
889            path.iter()
890                .map(|node| node.utf8_text(source.as_bytes()).unwrap())
891                .collect::<Vec<_>>(),
892            ["module", "function"]
893        );
894        let dynamic = crate::declarations::parse_python_tree("factory().function\n").unwrap();
895        let expression = dynamic
896            .root_node()
897            .named_child(0)
898            .unwrap()
899            .named_child(0)
900            .unwrap();
901        assert!(matches!(
902            super::python_static_value_path_bounded(expression, || true),
903            Some(None)
904        ));
905    }
906}