Skip to main content

brokk_bifrost_python/
structural.rs

1//! Python structural spec: maps tree-sitter-python node types onto the
2//! normalized kind vocabulary and extracts role edges from AST fields.
3//! See `src/analyzer/structural/spec.rs` for the contract and
4//! `.agent/ISSUE_328_SEARCH_AST_EXECPLAN.md` for the design.
5
6use brokk_bifrost_core::analyzer::common::node_source_text;
7use brokk_bifrost_core::analyzer::structural::adapter_helpers::{
8    CalleeSpineStep, attach_argument_role_with_derived_name, attach_role_with_derived_name,
9    attach_terminal_callee, field_name_in_parent, nearest_ancestor, node_range,
10    terminal_callee_leaf,
11};
12use brokk_bifrost_core::analyzer::structural::callable::CallSiteContext;
13use brokk_bifrost_core::analyzer::structural::edges::{
14    DEEP_REFERENCE_EDGE_SUPPORT, ReferenceEdgeSupport,
15};
16use brokk_bifrost_core::analyzer::structural::facts::{
17    AssignmentSequenceKind, AssignmentShape, IntegerLiteralValue, Span,
18    decode_integer_literal_digits,
19};
20use brokk_bifrost_core::analyzer::structural::kinds::{NormalizedKind, Role};
21use brokk_bifrost_core::analyzer::structural::materialization::{
22    DeclarationMaterializationSupport, PYTHON_MATERIALIZATION_SUPPORT, SourceExportBinding,
23    SourceExportBindingKind, SourceExportFact, SourceExportFacts, SourceExportLocalDefinition,
24};
25use brokk_bifrost_core::analyzer::structural::occurrences::{
26    Namespace, OccurrenceRole, OccurrenceRoleSupport, module_path_occurrence_namespace,
27};
28use brokk_bifrost_core::analyzer::structural::resolution::{
29    BindingActivation, BindingKind, DEEP_LEXICAL_ENVIRONMENT_SUPPORT, HoistingClass,
30    LexicalEnvironmentSupport, ScopeFormation, default_scope_formation,
31};
32use brokk_bifrost_core::analyzer::structural::routes::{
33    CuratedExportSurface, DEEP_IDENTITY_AXES, IdentityRouteSupport, RouteHopKind,
34};
35use brokk_bifrost_core::analyzer::structural::spec::{EmbeddedLeafFact, RoleSink, StructuralSpec};
36use brokk_bifrost_core::analyzer::{Language, Range};
37use brokk_bifrost_core::cancellation::CancellationToken;
38use brokk_bifrost_core::hash::{HashMap, HashSet};
39use tree_sitter::Node;
40
41use crate::bindings::{
42    PythonDirectScopeBindingKind, python_direct_scope_bindings_bounded,
43    python_executable_scope_children,
44};
45use crate::imports::python_import_bindings_from_tree;
46use crate::syntax::{
47    expression_name_node, python_conditional_expression_operands,
48    python_deferred_annotation_identifier_ranges, python_keyword_argument_label,
49    python_node_is_in_annotation, python_plain_string_literal,
50};
51
52#[derive(Debug, Default)]
53pub struct PythonStructuralSpec;
54
55pub static PYTHON_STRUCTURAL_SPEC: PythonStructuralSpec = PythonStructuralSpec;
56
57/// Tree-sitter interleaves grammar extras (`comment`, `line_continuation`)
58/// between the operands of a Python node wherever whitespace is allowed. A
59/// backslash newline is the `line_continuation` extra, so an operand that is
60/// selected by position among every named child can land on an extra instead.
61/// Structural roles and shapes describe the operands, so selection skips extras.
62fn python_first_named_operand(node: Node<'_>) -> Option<Node<'_>> {
63    let mut cursor = node.walk();
64    node.named_children(&mut cursor)
65        .find(|child| !child.is_extra())
66}
67
68pub(crate) fn python_parenthesized_expression_child(node: Node<'_>) -> Option<Node<'_>> {
69    assert_eq!(node.kind(), "parenthesized_expression");
70    let mut cursor = node.walk();
71    let mut children = node
72        .named_children(&mut cursor)
73        .filter(|child| !child.is_extra());
74    let child = children.next()?;
75    children.next().is_none().then_some(child)
76}
77
78fn python_unwrap_parenthesized(mut node: Node<'_>) -> Node<'_> {
79    while node.kind() == "parenthesized_expression" {
80        let Some(child) = python_parenthesized_expression_child(node) else {
81            break;
82        };
83        node = child;
84    }
85    node
86}
87
88fn python_unstarred_sequence_shape(node: Node<'_>, starred_node_kinds: &[&str]) -> (u32, bool) {
89    let mut unstarred_count = 0u32;
90    let mut starred = false;
91    for index in 0..node.named_child_count() {
92        let child = node
93            .named_child(index)
94            .expect("named child index is below the named child count");
95        if child.is_extra() {
96            continue;
97        }
98        match child.kind() {
99            kind if starred_node_kinds.contains(&kind) => starred = true,
100            _ => unstarred_count += 1,
101        }
102    }
103    (unstarred_count, starred)
104}
105
106/// Expand only displays whose argument names and cardinality are known from
107/// syntax. Validate the entire pack before attaching any ordinary arguments.
108fn python_literal_pack_arguments(argument: Node<'_>, sink: &mut RoleSink<'_>) -> bool {
109    let Some(operand) = python_first_named_operand(argument) else {
110        return false;
111    };
112    let operand = python_unwrap_parenthesized(operand);
113    match (argument.kind(), operand.kind()) {
114        ("list_splat", "tuple" | "list") => {
115            let mut cursor = operand.walk();
116            let elements: Vec<_> = operand
117                .named_children(&mut cursor)
118                .filter(|child| !child.is_extra())
119                .collect();
120            if elements
121                .iter()
122                .any(|element| element.kind() == "list_splat")
123            {
124                return false;
125            }
126            for element in elements {
127                attach_argument_role_with_derived_name(sink, element, expression_name_node);
128            }
129            true
130        }
131        ("dictionary_splat", "dictionary") => {
132            let mut entries = Vec::new();
133            let mut indices = HashMap::default();
134            let mut cursor = operand.walk();
135            for pair in operand
136                .named_children(&mut cursor)
137                .filter(|child| !child.is_extra())
138            {
139                if pair.kind() != "pair" {
140                    return false;
141                }
142                let (Some(key), Some(value)) = (
143                    pair.child_by_field_name("key"),
144                    pair.child_by_field_name("value"),
145                ) else {
146                    return false;
147                };
148                let Some(name) = python_plain_string_literal(key, sink.source()) else {
149                    return false;
150                };
151                // Plain strings have no escapes. Select their content using
152                // grammar nodes, including the zero-width content of "".
153                let mut key_cursor = key.walk();
154                let Some(content) = key
155                    .named_children(&mut key_cursor)
156                    .find(|child| matches!(child.kind(), "string_content" | "string_end"))
157                else {
158                    return false;
159                };
160                let span = Span {
161                    start_byte: content.start_byte(),
162                    end_byte: content.start_byte() + name.len(),
163                };
164                // Dict construction overwrites duplicate keys without moving
165                // their insertion position. Only the retained value is passed.
166                if let Some(&index) = indices.get(name) {
167                    entries[index] = (span, value);
168                } else {
169                    indices.insert(name, entries.len());
170                    entries.push((span, value));
171                }
172            }
173            for (keyword, value) in entries {
174                sink.kwarg_span(keyword, value);
175            }
176            true
177        }
178        _ => false,
179    }
180}
181
182fn python_assignment_target_shape(node: Node<'_>) -> Option<(AssignmentSequenceKind, u32, bool)> {
183    let node = python_unwrap_parenthesized(node);
184    let kind = match node.kind() {
185        "tuple_pattern" | "pattern_list" => AssignmentSequenceKind::Tuple,
186        "list_pattern" => AssignmentSequenceKind::List,
187        _ => return None,
188    };
189    let (count, starred) =
190        python_unstarred_sequence_shape(node, &["list_splat_pattern", "splat_pattern"]);
191    Some((kind, count, starred))
192}
193
194fn python_assignment_value_shape(node: Node<'_>) -> Option<(AssignmentSequenceKind, u32, bool)> {
195    let node = python_unwrap_parenthesized(node);
196    let kind = match node.kind() {
197        "tuple" | "expression_list" => AssignmentSequenceKind::Tuple,
198        "list" => AssignmentSequenceKind::List,
199        _ => return None,
200    };
201    let (count, starred) = python_unstarred_sequence_shape(node, &["list_splat"]);
202    Some((kind, count, starred))
203}
204
205fn python_assignment_shape(node: Node<'_>) -> Option<AssignmentShape> {
206    if node.kind() != "assignment" {
207        return None;
208    }
209    let (target_kind, target_unstarred_count, target_starred) =
210        python_assignment_target_shape(node.child_by_field_name("left")?)?;
211    let (value_kind, value_unstarred_count, value_starred) =
212        python_assignment_value_shape(node.child_by_field_name("right")?)?;
213    Some(AssignmentShape {
214        target_kind,
215        value_kind,
216        target_unstarred_count,
217        value_unstarred_count,
218        target_starred,
219        value_starred,
220    })
221}
222
223fn python_integer_literal_value(node: Node<'_>, source: &str) -> Option<IntegerLiteralValue> {
224    if node.kind() != "integer" {
225        return None;
226    }
227
228    let text = node_source_text(node, source).as_bytes();
229    let (radix, digits) = match (text.first(), text.get(1)) {
230        (Some(b'0'), Some(b'b' | b'B')) => (2u32, &text[2..]),
231        (Some(b'0'), Some(b'o' | b'O')) => (8u32, &text[2..]),
232        (Some(b'0'), Some(b'x' | b'X')) => (16u32, &text[2..]),
233        _ => (10u32, text),
234    };
235    decode_integer_literal_digits(radix, digits)
236}
237
238#[derive(Debug, PartialEq, Eq)]
239enum PythonDictionaryKeyValue {
240    String(String),
241    Integer { negative: bool, magnitude: String },
242    Boolean(bool),
243    None,
244}
245
246impl PythonDictionaryKeyValue {
247    fn equals(&self, other: &Self) -> bool {
248        match (self, other) {
249            (Self::String(left), Self::String(right)) => left == right,
250            (Self::None, Self::None) => true,
251            (
252                Self::Integer {
253                    negative: left_negative,
254                    magnitude: left_magnitude,
255                },
256                Self::Integer {
257                    negative: right_negative,
258                    magnitude: right_magnitude,
259                },
260            ) => left_negative == right_negative && left_magnitude == right_magnitude,
261            (Self::Boolean(value), Self::Boolean(other)) => value == other,
262            (
263                Self::Boolean(value),
264                Self::Integer {
265                    negative,
266                    magnitude,
267                },
268            )
269            | (
270                Self::Integer {
271                    negative,
272                    magnitude,
273                },
274                Self::Boolean(value),
275            ) => !negative && magnitude == if *value { "1" } else { "0" },
276            _ => false,
277        }
278    }
279}
280
281fn python_string_key_value(node: Node<'_>, source: &str) -> Option<String> {
282    if node.kind() == "concatenated_string" {
283        let mut value = String::new();
284        let mut cursor = node.walk();
285        for part in node
286            .named_children(&mut cursor)
287            .filter(|part| !part.is_extra())
288        {
289            if part.kind() != "string" {
290                return None;
291            }
292            value.push_str(&python_string_key_value(part, source)?);
293        }
294        return Some(value);
295    }
296    if node.kind() != "string" {
297        return None;
298    }
299
300    let mut raw = false;
301    let mut content = String::new();
302    let mut cursor = node.walk();
303    for child in node.children(&mut cursor) {
304        match child.kind() {
305            "string_start" => {
306                let start = node_source_text(child, source).as_bytes();
307                let prefix_end = start
308                    .iter()
309                    .position(|byte| matches!(byte, b'\'' | b'"'))
310                    .unwrap_or(start.len());
311                let prefix = std::str::from_utf8(&start[..prefix_end]).ok()?;
312                if !prefix.bytes().all(|byte| byte.is_ascii_alphabetic()) {
313                    return None;
314                }
315                let prefix = prefix.to_ascii_lowercase();
316                if prefix.contains('b') || prefix.contains('f') {
317                    return None;
318                }
319                raw = prefix.contains('r');
320            }
321            "string_content" => content.push_str(node_source_text(child, source)),
322            "escape_sequence" => content.push_str(node_source_text(child, source)),
323            "string_end" => {}
324            _ => return None,
325        }
326    }
327    python_decode_string_content(&content, raw)
328}
329
330fn python_decode_string_content(content: &str, raw: bool) -> Option<String> {
331    if raw {
332        return Some(content.to_owned());
333    }
334    let mut chars = content.chars().peekable();
335    let mut decoded = String::with_capacity(content.len());
336    while let Some(character) = chars.next() {
337        if character != '\\' {
338            decoded.push(character);
339            continue;
340        }
341        let escaped = chars.next()?;
342        let replacement = match escaped {
343            '\n' => continue,
344            '\r' => {
345                chars.next_if_eq(&'\n')?;
346                continue;
347            }
348            '\\' => '\\',
349            '\'' => '\'',
350            '"' => '"',
351            'a' => '\u{0007}',
352            'b' => '\u{0008}',
353            'f' => '\u{000c}',
354            'n' => '\n',
355            'r' => '\r',
356            't' => '\t',
357            'v' => '\u{000b}',
358            'x' => python_escape_codepoint(&mut chars, 2)?,
359            'u' => python_escape_codepoint(&mut chars, 4)?,
360            'U' => python_escape_codepoint(&mut chars, 8)?,
361            'N' => return None,
362            '0'..='7' => {
363                let mut value = escaped.to_digit(8)?;
364                for _ in 0..2 {
365                    let Some(next) = chars.peek().copied().filter(char::is_ascii_digit) else {
366                        break;
367                    };
368                    let Some(digit) = next.to_digit(8) else {
369                        break;
370                    };
371                    chars.next();
372                    value = value * 8 + digit;
373                }
374                char::from_u32(value)?
375            }
376            other => {
377                decoded.push('\\');
378                other
379            }
380        };
381        decoded.push(replacement);
382    }
383    Some(decoded)
384}
385
386fn python_escape_codepoint(
387    chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
388    digits: usize,
389) -> Option<char> {
390    let mut value = 0u32;
391    for _ in 0..digits {
392        value = value
393            .checked_mul(16)?
394            .checked_add(chars.next()?.to_digit(16)?)?;
395    }
396    char::from_u32(value)
397}
398
399fn python_dictionary_key_value(node: Node<'_>, source: &str) -> Option<PythonDictionaryKeyValue> {
400    let node = python_unwrap_parenthesized(node);
401    if matches!(node.kind(), "string" | "concatenated_string") {
402        return python_string_key_value(node, source).map(PythonDictionaryKeyValue::String);
403    }
404    if matches!(node.kind(), "true" | "false") {
405        return Some(PythonDictionaryKeyValue::Boolean(node.kind() == "true"));
406    }
407    if node.kind() == "none" {
408        return Some(PythonDictionaryKeyValue::None);
409    }
410
411    let (integer, negative) = if node.kind() == "unary_operator" {
412        let operator = node.child_by_field_name("operator")?;
413        match node_source_text(operator, source) {
414            "+" => (
415                python_unwrap_parenthesized(node.child_by_field_name("argument")?),
416                false,
417            ),
418            "-" => (
419                python_unwrap_parenthesized(node.child_by_field_name("argument")?),
420                true,
421            ),
422            _ => return None,
423        }
424    } else {
425        (node, false)
426    };
427    let value = python_integer_literal_value(integer, source)?;
428    let magnitude = value.decimal?;
429    let negative = negative && magnitude != "0";
430    Some(PythonDictionaryKeyValue::Integer {
431        negative,
432        magnitude,
433    })
434}
435
436fn python_duplicate_dictionary_key(node: Node<'_>, source: &str) -> Option<bool> {
437    if node.kind() != "pair" {
438        return None;
439    }
440    let dictionary = node
441        .parent()
442        .filter(|parent| parent.kind() == "dictionary")?;
443    let current_key = python_dictionary_key_value(node.child_by_field_name("key")?, source)?;
444    let mut earlier_unknown = false;
445    let mut all_entries = dictionary.walk();
446    let has_unpacked_entry = dictionary
447        .named_children(&mut all_entries)
448        .any(|entry| !entry.is_extra() && entry.kind() != "pair");
449    let mut cursor = dictionary.walk();
450    for entry in dictionary.named_children(&mut cursor) {
451        if entry.id() == node.id() {
452            return if earlier_unknown || has_unpacked_entry {
453                None
454            } else {
455                Some(false)
456            };
457        }
458        match entry.kind() {
459            "pair" => {
460                let previous_key = entry
461                    .child_by_field_name("key")
462                    .and_then(|key| python_dictionary_key_value(key, source));
463                if previous_key
464                    .as_ref()
465                    .is_some_and(|previous| current_key.equals(previous))
466                {
467                    return Some(true);
468                }
469                earlier_unknown |= previous_key.is_none();
470            }
471            _ if entry.is_extra() => {}
472            _ => {
473                earlier_unknown = true;
474            }
475        }
476    }
477    None
478}
479
480#[derive(Clone, Copy)]
481struct PythonComparisonShape<'tree> {
482    operator_count: usize,
483    operand_count: usize,
484    left: Option<Node<'tree>>,
485    right: Option<Node<'tree>>,
486    operator: Option<Node<'tree>>,
487}
488
489/// Read comparison operands and operator tokens from the grammar node. Python
490/// comparison operators are repeated fields; named operator aliases such as
491/// `is not` are distinguished from operands by their field name.
492fn python_comparison_shape(node: Node<'_>) -> PythonComparisonShape<'_> {
493    let mut operator_count = 0usize;
494    let mut operand_count = 0usize;
495    let mut left = None;
496    let mut right = None;
497    let mut operator = None;
498    visit_python_comparison_children(node, |child, is_operator| {
499        if is_operator {
500            operator_count += 1;
501            operator.get_or_insert(child);
502        } else if child.is_named() && !child.is_extra() {
503            operand_count += 1;
504            if left.is_none() {
505                left = Some(child);
506            } else if right.is_none() {
507                right = Some(child);
508            }
509        }
510        true
511    });
512    debug_assert!(
513        node.has_error() || operand_count == operator_count.saturating_add(1),
514        "well-formed Python comparison must have one more operand than operators: {node:?}"
515    );
516    PythonComparisonShape {
517        operator_count,
518        operand_count,
519        left,
520        right,
521        operator,
522    }
523}
524
525/// Shared grammar-field traversal for singular roles and transient domains.
526fn visit_python_comparison_children<'tree>(
527    node: Node<'tree>,
528    mut visit: impl FnMut(Node<'tree>, bool) -> bool,
529) -> bool {
530    let mut cursor = node.walk();
531    if cursor.goto_first_child() {
532        loop {
533            if !visit(cursor.node(), cursor.field_name() == Some("operators")) {
534                return false;
535            }
536            if !cursor.goto_next_sibling() {
537                break;
538            }
539        }
540    }
541    true
542}
543
544/// Whether a wrapper is a constant form used by comparison-of-constants:
545/// parenthesized scalar literals and unary signs over numeric literals. This
546/// deliberately does not fold arbitrary expressions such as `1 + 2`.
547fn python_comparison_constant(node: Node<'_>) -> bool {
548    let mut current = node;
549    let mut signed = false;
550    loop {
551        match current.kind() {
552            "parenthesized_expression" => {
553                let Some(expression) = python_parenthesized_expression_child(current) else {
554                    return false;
555                };
556                current = expression;
557            }
558            "unary_operator" => {
559                if !current
560                    .child_by_field_name("operator")
561                    .is_some_and(|operator| matches!(operator.kind(), "+" | "-"))
562                {
563                    return false;
564                }
565                let Some(argument) = current.child_by_field_name("argument") else {
566                    return false;
567                };
568                signed = true;
569                current = argument;
570            }
571            "integer" | "float" => return true,
572            "true" | "false" | "none" => return !signed,
573            "string" | "concatenated_string" => {
574                return !signed && python_plain_string_constant(current);
575            }
576            _ => return false,
577        }
578    }
579}
580
581/// Accept ordinary string tokens and implicit literal concatenation, but not
582/// interpolated strings. Tree structure, rather than source-text scanning,
583/// distinguishes the interpolation form.
584fn python_plain_string_constant(node: Node<'_>) -> bool {
585    let mut pending = vec![node];
586    let mut saw_string = false;
587    while let Some(current) = pending.pop() {
588        match current.kind() {
589            "string" => {
590                saw_string = true;
591                let mut cursor = current.walk();
592                if current
593                    .named_children(&mut cursor)
594                    .any(|child| child.kind() == "interpolation")
595                {
596                    return false;
597                }
598            }
599            "concatenated_string" => {
600                let mut cursor = current.walk();
601                let mut parts = 0;
602                for child in current.named_children(&mut cursor) {
603                    if child.kind() != "string" {
604                        return false;
605                    }
606                    parts += 1;
607                    pending.push(child);
608                }
609                if parts == 0 {
610                    return false;
611                }
612            }
613            _ => return false,
614        }
615    }
616    saw_string
617}
618
619/// Grammar node-type → normalized kind. Every name here must exist in the
620/// tree-sitter-python grammar; `tests::python_kind_table_matches_grammar`
621/// asserts that, so a grammar bump that renames a node fails loudly.
622pub const PYTHON_KIND_TABLE: &[(&str, NormalizedKind)] = &[
623    ("call", NormalizedKind::Call),
624    ("attribute", NormalizedKind::FieldAccess),
625    ("function_definition", NormalizedKind::Function),
626    ("lambda", NormalizedKind::Lambda),
627    ("await", NormalizedKind::Await),
628    ("yield", NormalizedKind::Yield),
629    ("module", NormalizedKind::Module),
630    ("class_definition", NormalizedKind::Class),
631    ("assignment", NormalizedKind::Assignment),
632    ("binary_operator", NormalizedKind::BinaryExpression),
633    ("boolean_operator", NormalizedKind::BinaryExpression),
634    ("comparison_operator", NormalizedKind::Comparison),
635    ("assert_statement", NormalizedKind::Assertion),
636    ("conditional_expression", NormalizedKind::If),
637    // These wrappers are emitted as literals only for supported constant
638    // forms; `should_extract` rejects computed or interpolated expressions.
639    ("parenthesized_expression", NormalizedKind::Literal),
640    ("unary_operator", NormalizedKind::Literal),
641    // The implicit `__exit__` of a `with` statement runs at the construct's
642    // own exit, so the statement is the source fact the resource-lifecycle
643    // selector names for that release.
644    ("with_statement", NormalizedKind::ResourceRelease),
645    ("import_statement", NormalizedKind::Import),
646    ("import_from_statement", NormalizedKind::Import),
647    ("identifier", NormalizedKind::Identifier),
648    ("string", NormalizedKind::StringLiteral),
649    ("concatenated_string", NormalizedKind::StringLiteral),
650    ("integer", NormalizedKind::NumericLiteral),
651    ("float", NormalizedKind::NumericLiteral),
652    ("true", NormalizedKind::BooleanLiteral),
653    ("false", NormalizedKind::BooleanLiteral),
654    ("none", NormalizedKind::NullLiteral),
655    ("return_statement", NormalizedKind::Return),
656    ("raise_statement", NormalizedKind::Throw),
657    ("except_clause", NormalizedKind::Catch),
658    ("if_statement", NormalizedKind::If),
659    ("for_statement", NormalizedKind::ForLoop),
660    ("list", NormalizedKind::ListLiteral),
661    ("set", NormalizedKind::CollectionLiteral),
662    ("dictionary", NormalizedKind::CollectionLiteral),
663    ("tuple", NormalizedKind::TupleLiteral),
664    ("pair", NormalizedKind::ObjectProperty),
665    ("while_statement", NormalizedKind::WhileLoop),
666    // Python's indented suite.
667    ("block", NormalizedKind::Block),
668    ("decorator", NormalizedKind::Decorator),
669    ("comment", NormalizedKind::Comment),
670];
671
672/// Attach `decorators` edges for a definition wrapped in Python's
673/// `decorated_definition` node (which itself is not normalized).
674fn attach_decorators(sink: &mut RoleSink<'_>, definition: Node<'_>) {
675    let Some(parent) = definition.parent() else {
676        return;
677    };
678    if parent.kind() != "decorated_definition" {
679        return;
680    }
681    for index in 0..parent.named_child_count() {
682        let Some(child) = parent.named_child(index) else {
683            continue;
684        };
685        if child.kind() == "decorator" {
686            attach_role_with_derived_name(sink, Role::Decorator, child, expression_name_node);
687        }
688    }
689}
690
691static PYTHON_OCCURRENCE_ROLE_SUPPORT: OccurrenceRoleSupport = OccurrenceRoleSupport::NONE
692    .supported(OccurrenceRole::DeclarationName)
693    .supported(OccurrenceRole::Binder)
694    .supported(OccurrenceRole::LabelOrKey)
695    .supported(OccurrenceRole::TypeOperand)
696    .supported(OccurrenceRole::PathSegment)
697    .supported(OccurrenceRole::ImportAlias)
698    .supported(OccurrenceRole::ImportTarget)
699    .supported(OccurrenceRole::ReceiverPosition)
700    .supported(OccurrenceRole::MemberPosition)
701    .supported(OccurrenceRole::ValueReference)
702    .supported(OccurrenceRole::GlobalDirective)
703    .supported(OccurrenceRole::NonlocalDirective);
704
705/// Whether a `dotted_name` names an imported module rather than an ordinary
706/// attribute chain, which decides whether its tail is an import target.
707///
708/// `relative_import` is the wrapper the grammar puts around the module name of
709/// `from .impl import x`. It is the same import target as the `pkg.impl` of
710/// `from pkg.impl import x`, so it passes through like the other wrappers.
711fn python_dotted_name_is_import(dotted_name: Node<'_>) -> bool {
712    let mut current = dotted_name;
713    loop {
714        let Some(parent) = current.parent() else {
715            return false;
716        };
717        match parent.kind() {
718            "import_statement" | "import_from_statement" | "future_import_statement" => {
719                return true;
720            }
721            "aliased_import" | "dotted_name" | "relative_import" => current = parent,
722            _ => return false,
723        }
724    }
725}
726
727/// Classify one Python identifier token by its AST position.
728///
729/// Python has no separate type-identifier node, so annotation operands are
730/// recognized by their enclosing `type` node — the same field the parser uses
731/// to separate `def f(x: T)`'s binder from its annotation.
732fn python_occurrence_role(node: Node<'_>) -> Option<OccurrenceRole> {
733    if node.kind() != "identifier" {
734        return None;
735    }
736    let parent = node.parent()?;
737    let field = field_name_in_parent(parent, node);
738    let role = match parent.kind() {
739        "global_statement" => OccurrenceRole::GlobalDirective,
740        "nonlocal_statement" => OccurrenceRole::NonlocalDirective,
741        "function_definition" | "class_definition" if field == Some("name") => {
742            OccurrenceRole::DeclarationName
743        }
744        // Every annotation, return type and type-alias operand is wrapped in a
745        // `type` node; `generic_type`/`type_parameter` nest inside one.
746        "type" | "generic_type" | "type_parameter" | "constrained_type" | "union_type" => {
747            OccurrenceRole::TypeOperand
748        }
749        "parameters"
750        | "lambda_parameters"
751        | "typed_parameter"
752        | "list_splat_pattern"
753        | "dictionary_splat_pattern"
754        | "tuple_pattern"
755        | "list_pattern"
756        | "pattern_list"
757        | "as_pattern_target" => OccurrenceRole::Binder,
758        "default_parameter" | "typed_default_parameter" if field == Some("name") => {
759            OccurrenceRole::Binder
760        }
761        "for_statement" | "for_in_clause" if field == Some("left") => OccurrenceRole::Binder,
762        "assignment" if field == Some("left") => OccurrenceRole::Binder,
763        "keyword_argument" if python_keyword_argument_label(node) => OccurrenceRole::LabelOrKey,
764        "attribute" => match field {
765            Some("attribute") => OccurrenceRole::MemberPosition,
766            Some("object") => OccurrenceRole::ReceiverPosition,
767            _ => OccurrenceRole::ValueReference,
768        },
769        "aliased_import" if field == Some("alias") => OccurrenceRole::ImportAlias,
770        "import_from_statement" if field == Some("name") => OccurrenceRole::ImportTarget,
771        "dotted_name" => {
772            let is_tail =
773                parent.named_child(parent.named_child_count().saturating_sub(1)) == Some(node);
774            match (is_tail, python_dotted_name_is_import(parent)) {
775                (true, true) => OccurrenceRole::ImportTarget,
776                (true, false) => OccurrenceRole::ValueReference,
777                (false, _) => OccurrenceRole::PathSegment,
778            }
779        }
780        _ => OccurrenceRole::ValueReference,
781    };
782    Some(role)
783}
784
785/// Whether `node` is the identifier token that binds one formal parameter.
786///
787/// Tree-sitter uses a bare identifier directly under `parameters` or
788/// `lambda_parameters`, and wrapper nodes for annotations, defaults, and
789/// splats. The shared label helper selects the exact name child for each
790/// wrapper so identifiers in annotations/default values do not become
791/// parameter facts.
792fn python_identifier_is_parameter(node: Node<'_>) -> bool {
793    if node.kind() != "identifier" {
794        return false;
795    }
796    let Some(parent) = node.parent() else {
797        return false;
798    };
799    match parent.kind() {
800        "parameters" | "lambda_parameters" => true,
801        "typed_parameter"
802        | "default_parameter"
803        | "typed_default_parameter"
804        | "list_splat_pattern"
805        | "dictionary_splat_pattern"
806        | "keyword_separator" => {
807            crate::declarations::python_parameter_label_node(parent) == Some(node)
808        }
809        _ => false,
810    }
811}
812
813/// Whether a `def` declares a method, that is, whether the suite it sits in
814/// belongs to a `class_definition`.
815///
816/// This reads the parse tree rather than the nearest enclosing normalized kind
817/// because the suite between a class and its methods is itself a normalized
818/// node now (`NormalizedKind::Block`, issue #1474). Walking the concrete
819/// ancestors is also the more direct statement of the rule: a nested `def`
820/// inside a method reaches its enclosing `function_definition` first and stays
821/// a function.
822fn python_definition_is_method(definition: Node<'_>) -> bool {
823    let mut current = definition;
824    while let Some(parent) = current.parent() {
825        match parent.kind() {
826            "class_definition" => return true,
827            // The suite that holds the members, and the wrapper a decorated
828            // definition sits in, are pass-through on the way to the owner.
829            "block" | "decorated_definition" => current = parent,
830            _ => return false,
831        }
832    }
833    false
834}
835
836/// The binding one Python binder token introduces, and the interval it is in
837/// effect over.
838///
839/// Python's function locals are scope-categorical rather than positional: a
840/// name assigned anywhere in a function body is a local of that function for
841/// the whole body, which is why a read above the assignment is an
842/// `UnboundLocalError` rather than a read of an outer name. That is exactly
843/// `ScopeWide`. The one positional exception is a comprehension target, which
844/// lives in the comprehension's own implicit scope; the same exception
845/// `analyzer::python::bindings` records as `PythonComprehensionBinding`.
846fn python_binding_activation(binder: Node<'_>, scope: Range) -> Option<BindingActivation> {
847    // A binder outside every form below is an ordinary assignment target --
848    // the members of a module-level or block-level `a, b = ...` pattern list,
849    // for example -- and Python's scope-categorical rule makes it a local of
850    // its declaring scope for the whole scope. Answering `None` here made the
851    // whole file's lexical environment incomplete as soon as one such binder
852    // existed (scripts/test-cost/greedy.py's `s, k = heapq.heappop(heap)` at
853    // module scope), which failed every code-smells gate that derived it.
854    let Some(form) = nearest_ancestor(binder, |kind| {
855        matches!(
856            kind,
857            "parameters"
858                | "lambda_parameters"
859                | "assignment"
860                | "for_statement"
861                | "for_in_clause"
862                | "as_pattern"
863                | "list_comprehension"
864                | "set_comprehension"
865                | "dictionary_comprehension"
866                | "generator_expression"
867        )
868    }) else {
869        return Some(BindingActivation {
870            kind: BindingKind::Local,
871            hoisting: HoistingClass::ScopeWide,
872            activation: scope,
873        });
874    };
875    match form.kind() {
876        "parameters" | "lambda_parameters" => Some(BindingActivation {
877            kind: BindingKind::Parameter,
878            hoisting: HoistingClass::ScopeWide,
879            activation: scope,
880        }),
881        "assignment" => Some(BindingActivation {
882            kind: BindingKind::Local,
883            hoisting: HoistingClass::ScopeWide,
884            activation: scope,
885        }),
886        "for_in_clause" => {
887            // A comprehension clause binds only inside the comprehension.
888            let comprehension = nearest_ancestor(form, |kind| {
889                matches!(
890                    kind,
891                    "list_comprehension"
892                        | "set_comprehension"
893                        | "dictionary_comprehension"
894                        | "generator_expression"
895                )
896            })?;
897            Some(BindingActivation {
898                kind: BindingKind::LoopVariable,
899                hoisting: HoistingClass::DeclaredHead,
900                activation: node_range(comprehension),
901            })
902        }
903        "for_statement" => Some(BindingActivation {
904            kind: BindingKind::LoopVariable,
905            hoisting: HoistingClass::ScopeWide,
906            activation: scope,
907        }),
908        "as_pattern" => Some(BindingActivation {
909            kind: BindingKind::PatternBinder,
910            hoisting: HoistingClass::ScopeWide,
911            activation: scope,
912        }),
913        _ => Some(BindingActivation {
914            kind: BindingKind::Local,
915            hoisting: HoistingClass::ScopeWide,
916            activation: scope,
917        }),
918    }
919}
920
921/// The text one plain string literal denotes, or `None` when the literal is
922/// not plain: an f-string interpolation, an escape sequence this reader does
923/// not decode, or an implicit concatenation. `None` is never an empty name --
924/// it is "this value is computed", which makes the whole surface unreadable.
925/// Whether every member of a curated surface was read from the parse tree.
926#[derive(Debug, Clone, Copy, PartialEq, Eq)]
927enum ReadableSurface {
928    Yes,
929    No,
930}
931
932#[derive(Debug, Clone)]
933struct PythonExportMember {
934    name: String,
935    range: Range,
936}
937
938/// Collect the members of an `__all__` value into `names`, reporting whether
939/// every member was readable. Only a list or tuple display of plain string
940/// literals is; anything else is a value the source computes.
941fn python_collect_all_members(
942    value: Node<'_>,
943    source: &str,
944    members: &mut Vec<PythonExportMember>,
945) -> ReadableSurface {
946    if !matches!(value.kind(), "list" | "tuple") {
947        return ReadableSurface::No;
948    }
949    let mut cursor = value.walk();
950    for element in value
951        .named_children(&mut cursor)
952        .filter(|element| !element.is_extra())
953    {
954        match python_plain_string_literal(element, source) {
955            Some(text) => {
956                members.push(PythonExportMember {
957                    name: text.to_owned(),
958                    range: node_range(element),
959                });
960            }
961            None => return ReadableSurface::No,
962        }
963    }
964    ReadableSurface::Yes
965}
966
967fn python_module_export_bindings(
968    root: Node<'_>,
969    source: &str,
970) -> HashMap<Box<str>, SourceExportBinding> {
971    let mut bindings = Vec::new();
972    let mut stack = vec![root];
973    while let Some(node) = stack.pop() {
974        for binding in python_direct_scope_bindings_bounded(node, source, || true)
975            .expect("unbounded Python module binding scan completes")
976        {
977            let name = node_source_text(binding.declaration, source);
978            if name.is_empty() {
979                continue;
980            }
981            let kind = match binding.kind {
982                PythonDirectScopeBindingKind::ClassDeclaration => SourceExportBindingKind::Class,
983                PythonDirectScopeBindingKind::FunctionDeclaration => {
984                    SourceExportBindingKind::Function
985                }
986                PythonDirectScopeBindingKind::Import => SourceExportBindingKind::Import,
987                PythonDirectScopeBindingKind::Other => SourceExportBindingKind::Binding,
988            };
989            bindings.push(SourceExportBinding {
990                name: name.to_owned(),
991                kind,
992                range: node_range(binding.declaration),
993            });
994        }
995        stack.extend(python_executable_scope_children(node));
996    }
997    bindings.sort_unstable_by_key(|binding| binding.range.start_byte);
998    let mut by_name = HashMap::default();
999    for binding in bindings {
1000        by_name
1001            .entry(binding.name.clone().into())
1002            .or_insert(binding);
1003    }
1004    by_name
1005}
1006
1007fn python_call_function_name<'a>(node: Node<'_>, source: &'a str) -> Option<&'a str> {
1008    if node.kind() != "call" {
1009        return None;
1010    }
1011    let function = node.child_by_field_name("function")?;
1012    (function.kind() == "identifier").then(|| node_source_text(function, source))
1013}
1014
1015fn python_current_module_expression(
1016    expression: Node<'_>,
1017    source: &str,
1018    sys_bindings: &HashSet<Box<str>>,
1019    module_aliases: &HashSet<Box<str>>,
1020) -> bool {
1021    let expression = python_unwrap_parenthesized(expression);
1022    if expression.kind() == "identifier"
1023        && module_aliases.contains(node_source_text(expression, source))
1024    {
1025        return true;
1026    }
1027    if expression.kind() == "subscript"
1028        && let (Some(value), Some(subscript)) = (
1029            expression.child_by_field_name("value"),
1030            expression.child_by_field_name("subscript"),
1031        )
1032        && subscript.kind() == "identifier"
1033        && node_source_text(subscript, source) == "__name__"
1034        && value.kind() == "attribute"
1035        && value
1036            .child_by_field_name("attribute")
1037            .is_some_and(|attribute| node_source_text(attribute, source) == "modules")
1038        && value.child_by_field_name("object").is_some_and(|object| {
1039            object.kind() == "identifier" && sys_bindings.contains(node_source_text(object, source))
1040        })
1041    {
1042        return true;
1043    }
1044    python_call_function_name(expression, source) == Some("__import__")
1045        && expression
1046            .child_by_field_name("arguments")
1047            .and_then(python_first_named_operand)
1048            .is_some_and(|argument| {
1049                argument.kind() == "identifier" && node_source_text(argument, source) == "__name__"
1050            })
1051}
1052
1053fn python_module_definition_is_dynamic(root: Node<'_>, source: &str) -> bool {
1054    let sys_bindings: HashSet<Box<str>> = python_import_bindings_from_tree(root, source)
1055        .into_iter()
1056        .filter(|binding| binding.qualified_name == "sys")
1057        .map(|binding| binding.local_name.into_boxed_str())
1058        .collect();
1059    let mut assignments = Vec::new();
1060    let mut pending = vec![root];
1061    while let Some(node) = pending.pop() {
1062        if node.kind() == "assignment"
1063            && let (Some(left), Some(right)) = (
1064                node.child_by_field_name("left"),
1065                node.child_by_field_name("right"),
1066            )
1067            && left.kind() == "identifier"
1068        {
1069            assignments.push((node.start_byte(), left, right));
1070        }
1071        let mut cursor = node.walk();
1072        pending.extend(node.named_children(&mut cursor));
1073    }
1074    assignments.sort_unstable_by_key(|(start, _, _)| *start);
1075    let mut module_aliases = HashSet::default();
1076    for (_, left, right) in assignments {
1077        if python_current_module_expression(right, source, &sys_bindings, &module_aliases) {
1078            module_aliases.insert(node_source_text(left, source).into());
1079        }
1080    }
1081
1082    let mut pending = vec![root];
1083    while let Some(node) = pending.pop() {
1084        if node.kind() == "wildcard_import"
1085            || python_call_function_name(node, source) == Some("globals")
1086        {
1087            return true;
1088        }
1089        if python_call_function_name(node, source) == Some("setattr")
1090            && node
1091                .child_by_field_name("arguments")
1092                .and_then(python_first_named_operand)
1093                .is_some_and(|object| {
1094                    python_current_module_expression(object, source, &sys_bindings, &module_aliases)
1095                })
1096        {
1097            return true;
1098        }
1099        let mut cursor = node.walk();
1100        pending.extend(node.named_children(&mut cursor));
1101    }
1102    root.has_error()
1103}
1104
1105/// Parse the source-backed member rows for the module's explicit `__all__`.
1106///
1107/// Known literals are retained if another member makes the complete surface
1108/// unreadable. The materialization query then returns those useful rows and
1109/// reports that absence of additional rows is not proof of completeness.
1110fn python_source_export_facts(root: Node<'_>, source: &str) -> SourceExportFacts {
1111    let names_all = |node: Option<Node<'_>>| {
1112        node.is_some_and(|node| {
1113            node.kind() == "identifier" && node_source_text(node, source) == "__all__"
1114        })
1115    };
1116    let mut members = Vec::new();
1117    let mut stated = false;
1118    let mut readable = ReadableSurface::Yes;
1119    let module_bindings = python_module_export_bindings(root, source);
1120    let definitions_complete = !python_module_definition_is_dynamic(root, source);
1121    let mut cursor = root.walk();
1122    for statement in root.named_children(&mut cursor) {
1123        if statement.kind() == "ERROR"
1124            && crate::syntax::python_contains_all_identifier(statement, source)
1125        {
1126            stated = true;
1127            readable = ReadableSurface::No;
1128            continue;
1129        }
1130        if statement.kind() != "expression_statement" {
1131            continue;
1132        }
1133        let mut inner = statement.walk();
1134        for expression in statement.named_children(&mut inner) {
1135            match expression.kind() {
1136                "ERROR" if crate::syntax::python_contains_all_identifier(expression, source) => {
1137                    stated = true;
1138                    readable = ReadableSurface::No;
1139                }
1140                "assignment" | "augmented_assignment" => {
1141                    if !names_all(expression.child_by_field_name("left")) {
1142                        continue;
1143                    }
1144                    stated = true;
1145                    let Some(value) = expression.child_by_field_name("right") else {
1146                        let mut cursor = expression.walk();
1147                        let has_assignment_operator = expression
1148                            .children(&mut cursor)
1149                            .any(|child| child.kind() == "=");
1150                        let annotation_only = expression.kind() == "assignment"
1151                            && expression.child_by_field_name("type").is_some()
1152                            && !has_assignment_operator
1153                            && !expression.has_error();
1154                        if !annotation_only {
1155                            readable = ReadableSurface::No;
1156                        }
1157                        continue;
1158                    };
1159                    if expression.kind() == "assignment" {
1160                        members.clear();
1161                        readable = ReadableSurface::Yes;
1162                    }
1163                    let extends = expression.kind() == "assignment"
1164                        || expression
1165                            .child_by_field_name("operator")
1166                            .is_some_and(|operator| operator.kind() == "+=");
1167                    if !extends
1168                        || python_collect_all_members(value, source, &mut members)
1169                            == ReadableSurface::No
1170                    {
1171                        readable = ReadableSurface::No;
1172                    }
1173                }
1174                "call" => {
1175                    let Some(function) = expression.child_by_field_name("function") else {
1176                        continue;
1177                    };
1178                    if function.kind() == "attribute"
1179                        && names_all(function.child_by_field_name("object"))
1180                    {
1181                        stated = true;
1182                        readable = ReadableSurface::No;
1183                    }
1184                }
1185                _ => {}
1186            }
1187        }
1188    }
1189
1190    let mut names = HashSet::default();
1191    let exports = if stated {
1192        members
1193            .into_iter()
1194            .filter(|member| names.insert(member.name.clone()))
1195            .map(|member| {
1196                let local_definition = match module_bindings.get(member.name.as_str()) {
1197                    Some(binding) => SourceExportLocalDefinition::Defined {
1198                        binding: binding.clone(),
1199                    },
1200                    None if definitions_complete => SourceExportLocalDefinition::Undefined,
1201                    None => SourceExportLocalDefinition::Unknown,
1202                };
1203                SourceExportFact {
1204                    range: member.range,
1205                    exported_name: member.name,
1206                    local_definition,
1207                }
1208            })
1209            .collect()
1210    } else {
1211        Vec::new()
1212    };
1213    SourceExportFacts {
1214        exports,
1215        declared: stated,
1216        complete: !stated || readable == ReadableSurface::Yes,
1217        definitions_complete,
1218    }
1219}
1220
1221/// The names a Python module curates as its public surface: the value of its
1222/// module-level `__all__`.
1223///
1224/// Only the module's own statements are read. An `__all__` inside a function
1225/// is a local rather than the module's surface, and a name the module binds
1226/// conditionally is still bound by the statement this reader already sees.
1227/// A statement that assigns, extends, or mutates `__all__` with anything but a
1228/// list or tuple of plain string literals makes the surface unreadable: the
1229/// members are then unknown, and no import is classified from them.
1230fn python_curated_export_surface(root: Node<'_>, source: &str) -> CuratedExportSurface {
1231    let facts = python_source_export_facts(root, source);
1232    match (facts.declared, facts.complete) {
1233        (false, _) => CuratedExportSurface::Absent,
1234        (true, false) => CuratedExportSurface::Unreadable,
1235        (true, true) => CuratedExportSurface::Listed(
1236            facts
1237                .exports
1238                .into_iter()
1239                .map(|export| export.exported_name)
1240                .collect(),
1241        ),
1242    }
1243}
1244
1245/// Which indirection relation one Python import token participates in.
1246///
1247/// Python's grammar names no re-export, so the relation follows the explicit
1248/// re-export rules the typing ecosystem already enforces (PEP 484 stub
1249/// semantics, applied by pyright and mypy in strict mode), which makes this
1250/// relation agree with what a type checker calls public:
1251///
1252/// 1. A name on the module's `__all__` is a re-export of whatever binding the
1253///    module gives that name.
1254/// 2. The redundant-alias forms `from x import y as y` and `import x as x`
1255///    are re-exports.
1256/// 3. `from x import *` is one star hop that forwards the public surface of
1257///    `x`; the expansion is the import machinery's work, not this producer's,
1258///    so the hop is recorded on the module reference and nothing is
1259///    enumerated here.
1260/// 4. Every other import is an ordinary import, including a plain
1261///    `from .impl import helper` in a package `__init__.py` that states no
1262///    `__all__`. The facade convention alone does not make a name public, and
1263///    a consumer that wants every name a facade imports already has the
1264///    import relation.
1265///
1266/// `None` is the answer for a name whose membership only an unreadable
1267/// `__all__` could settle; the file's relations then report incomplete rather
1268/// than guessing either way.
1269fn python_indirection_relation(
1270    token: Node<'_>,
1271    source: &str,
1272    surface: &CuratedExportSurface,
1273) -> Option<RouteHopKind> {
1274    let statement = nearest_ancestor(token, |kind| {
1275        matches!(
1276            kind,
1277            "import_statement" | "import_from_statement" | "future_import_statement"
1278        )
1279    })?;
1280    // The statement's own child that holds this token: a `module_name` field,
1281    // or one `name` field of the import list.
1282    let mut clause = token;
1283    while let Some(parent) = clause.parent() {
1284        if parent.id() == statement.id() {
1285            break;
1286        }
1287        clause = parent;
1288    }
1289
1290    if field_name_in_parent(statement, clause) == Some("module_name") {
1291        // `from x import a` binds `a`, not `x`, so the module reference
1292        // forwards nothing -- unless the import is the star form, whose one
1293        // hop forwards the whole surface of `x`.
1294        let mut cursor = statement.walk();
1295        let star = statement
1296            .children(&mut cursor)
1297            .any(|child| child.kind() == "wildcard_import");
1298        return Some(if star {
1299            RouteHopKind::ReExport
1300        } else {
1301            RouteHopKind::Import
1302        });
1303    }
1304
1305    let bound = match clause.kind() {
1306        "aliased_import" => {
1307            let name = clause.child_by_field_name("name")?;
1308            let alias = clause.child_by_field_name("alias")?;
1309            if node_source_text(name, source) == node_source_text(alias, source) {
1310                return Some(RouteHopKind::ReExport);
1311            }
1312            alias
1313        }
1314        // `import a.b.c` binds the top package `a`; `from m import a` binds
1315        // the single-segment name the import list spells.
1316        "dotted_name" if statement.kind() == "import_statement" => clause.named_child(0)?,
1317        "dotted_name" => clause,
1318        _ => return None,
1319    };
1320    match surface.lists(node_source_text(bound, source)) {
1321        Some(true) => Some(RouteHopKind::ReExport),
1322        Some(false) => Some(RouteHopKind::Import),
1323        None => None,
1324    }
1325}
1326
1327/// Grammar steps for the shared call-callee spine. A value expression has no step.
1328pub fn callee_spine_step(kind: &str) -> Option<CalleeSpineStep> {
1329    use CalleeSpineStep::{Field, Name};
1330    match kind {
1331        "identifier" => Some(Name),
1332        "attribute" => Some(Field("attribute")),
1333        _ => None,
1334    }
1335}
1336
1337impl StructuralSpec for PythonStructuralSpec {
1338    fn language(&self) -> Language {
1339        Language::Python
1340    }
1341
1342    fn supports_boolean_literal_value(&self) -> bool {
1343        true
1344    }
1345
1346    fn supports_string_literal_parenthesized(&self) -> bool {
1347        true
1348    }
1349
1350    fn supports_string_literal_value(&self) -> bool {
1351        true
1352    }
1353
1354    fn string_literal_value(&self, node: Node<'_>, source: &str) -> Option<String> {
1355        python_string_key_value(node, source)
1356    }
1357
1358    fn supports_assignment_shape(&self) -> bool {
1359        true
1360    }
1361
1362    fn assignment_shape(&self, node: Node<'_>) -> Option<AssignmentShape> {
1363        python_assignment_shape(node)
1364    }
1365
1366    fn supports_integer_literal_value(&self) -> bool {
1367        true
1368    }
1369
1370    fn integer_literal_value(&self, node: Node<'_>, source: &str) -> Option<IntegerLiteralValue> {
1371        python_integer_literal_value(node, source)
1372    }
1373
1374    fn supports_duplicate_dictionary_keys(&self) -> bool {
1375        true
1376    }
1377
1378    fn duplicate_dictionary_key(&self, node: Node<'_>, source: &str) -> Option<bool> {
1379        python_duplicate_dictionary_key(node, source)
1380    }
1381
1382    fn string_literal_parenthesized(&self, node: Node<'_>) -> Option<bool> {
1383        if !matches!(node.kind(), "string" | "concatenated_string") {
1384            return None;
1385        }
1386        let mut current = node;
1387        let mut parenthesized = false;
1388        while let Some(parent) = current.parent() {
1389            if parent.kind() != "parenthesized_expression" {
1390                break;
1391            }
1392            assert_eq!(python_parenthesized_expression_child(parent), Some(current));
1393            current = parent;
1394            parenthesized = true;
1395        }
1396        Some(parenthesized)
1397    }
1398
1399    fn reference_edge_support(&self) -> &ReferenceEdgeSupport {
1400        &DEEP_REFERENCE_EDGE_SUPPORT
1401    }
1402
1403    fn identity_route_support(&self) -> &IdentityRouteSupport {
1404        // `import x as y` is an alias, and `python_indirection_relation`
1405        // states which imports re-export (issue #1649).
1406        static SUPPORT: IdentityRouteSupport = DEEP_IDENTITY_AXES
1407            .supported_relation(RouteHopKind::Alias)
1408            .supported_relation(RouteHopKind::Import)
1409            .supported_relation(RouteHopKind::ReExport)
1410            .supported_relation(RouteHopKind::NestedOwner);
1411        &SUPPORT
1412    }
1413
1414    /// Python's one qualified-path chain is `dotted_name`, which is flat
1415    /// rather than left-nested: its named children are the segments in order.
1416    fn qualified_path_root<'tree>(&self, token: Node<'tree>) -> Option<Node<'tree>> {
1417        if token.kind() != "identifier" {
1418            return None;
1419        }
1420        token
1421            .parent()
1422            .filter(|parent| parent.kind() == "dotted_name")
1423    }
1424
1425    fn path_segment_tokens<'tree>(&self, root: Node<'tree>) -> Vec<Node<'tree>> {
1426        if root.kind() != "dotted_name" {
1427            return Vec::new();
1428        }
1429        let mut cursor = root.walk();
1430        root.named_children(&mut cursor)
1431            .filter(|child| child.kind() == "identifier")
1432            .collect()
1433    }
1434
1435    fn curated_export_surface(&self, root: Node<'_>, source: &str) -> CuratedExportSurface {
1436        python_curated_export_surface(root, source)
1437    }
1438
1439    fn indirection_relation(
1440        &self,
1441        token: Node<'_>,
1442        source: &str,
1443        surface: &CuratedExportSurface,
1444    ) -> Option<RouteHopKind> {
1445        python_indirection_relation(token, source, surface)
1446    }
1447
1448    fn scope_formation(&self, kind: NormalizedKind) -> ScopeFormation {
1449        match kind {
1450            // The normalized module fact is the file root, not a nested scope.
1451            // Top-level imports and assignments belong to the synthetic file scope.
1452            NormalizedKind::Module => ScopeFormation::NotAScope,
1453            // Python blocks do not introduce local-variable scopes. Names
1454            // assigned in an if/loop/try body remain function or module locals.
1455            NormalizedKind::Block | NormalizedKind::ForLoop | NormalizedKind::WhileLoop => {
1456                ScopeFormation::NotAScope
1457            }
1458            _ => default_scope_formation(kind),
1459        }
1460    }
1461
1462    fn kind_table(&self) -> &'static [(&'static str, NormalizedKind)] {
1463        PYTHON_KIND_TABLE
1464    }
1465
1466    fn refine_kind(
1467        &self,
1468        node: Node<'_>,
1469        kind: NormalizedKind,
1470        _enclosing: Option<NormalizedKind>,
1471        _source: &str,
1472        _context: &CallSiteContext,
1473    ) -> NormalizedKind {
1474        match kind {
1475            NormalizedKind::Identifier if python_identifier_is_parameter(node) => {
1476                NormalizedKind::Parameter
1477            }
1478            NormalizedKind::Function if python_definition_is_method(node) => NormalizedKind::Method,
1479            NormalizedKind::Comparison if python_comparison_shape(node).operator_count > 1 => {
1480                NormalizedKind::ComparisonChain
1481            }
1482            _ => kind,
1483        }
1484    }
1485
1486    fn should_extract(&self, node: Node<'_>, kind: NormalizedKind) -> bool {
1487        if kind == NormalizedKind::Module && node.byte_range().is_empty() {
1488            return false;
1489        }
1490        match node.kind() {
1491            "parenthesized_expression" | "unary_operator" => {
1492                kind == NormalizedKind::Literal && python_comparison_constant(node)
1493            }
1494            _ => kind != NormalizedKind::Assignment || node.child_by_field_name("right").is_some(),
1495        }
1496    }
1497
1498    fn supports_kind(&self, kind: NormalizedKind) -> bool {
1499        brokk_bifrost_core::analyzer::structural::spec::supports_kind_from_table(
1500            kind,
1501            &[
1502                NormalizedKind::Method,
1503                // Module facts select the parser's file root for Python policies.
1504                NormalizedKind::Module,
1505                NormalizedKind::Parameter,
1506                NormalizedKind::SyntaxError,
1507                NormalizedKind::ComparisonChain,
1508                NormalizedKind::RegexPattern,
1509                NormalizedKind::RegexSequence,
1510                NormalizedKind::RegexGroup,
1511                NormalizedKind::RegexAlternation,
1512                NormalizedKind::RegexCharacterClass,
1513                NormalizedKind::RegexCharacterRange,
1514                NormalizedKind::RegexQuantifier,
1515                NormalizedKind::RegexAnchor,
1516                NormalizedKind::RegexEscape,
1517                NormalizedKind::RegexLiteral,
1518                NormalizedKind::RegexWildcard,
1519                NormalizedKind::RegexRiskHostname,
1520                NormalizedKind::RegexRiskRange,
1521                NormalizedKind::RegexRiskBadTag,
1522                NormalizedKind::RegexRiskExponential,
1523                NormalizedKind::RegexIncomplete,
1524                NormalizedKind::RegexRiskBackspaceEscape,
1525                NormalizedKind::RegexRiskDuplicateClassCharacter,
1526                NormalizedKind::RegexRiskUnmatchableCaret,
1527                NormalizedKind::RegexRiskUnmatchableDollar,
1528                NormalizedKind::RegexRiskIncompleteSpecialGroup,
1529                NormalizedKind::RegexRiskPolynomial,
1530            ],
1531            self.kind_table(),
1532        )
1533    }
1534
1535    fn occurrence_role_support(&self) -> &OccurrenceRoleSupport {
1536        &PYTHON_OCCURRENCE_ROLE_SUPPORT
1537    }
1538
1539    fn lexical_environment_support(&self) -> &LexicalEnvironmentSupport {
1540        &DEEP_LEXICAL_ENVIRONMENT_SUPPORT
1541    }
1542
1543    fn materialization_support(&self) -> &DeclarationMaterializationSupport {
1544        &PYTHON_MATERIALIZATION_SUPPORT
1545    }
1546
1547    fn source_export_facts(&self, root: Node<'_>, source: &str) -> Option<SourceExportFacts> {
1548        Some(python_source_export_facts(root, source))
1549    }
1550
1551    fn binding_activation(&self, binder: Node<'_>, scope: Range) -> Option<BindingActivation> {
1552        python_binding_activation(binder, scope)
1553    }
1554
1555    /// Python only classifies a scope segment inside a `dotted_name`, and every
1556    /// non-tail segment of a dotted name is a module.
1557    fn occurrence_namespace(
1558        &self,
1559        role: OccurrenceRole,
1560        declares: Option<NormalizedKind>,
1561    ) -> Option<Namespace> {
1562        module_path_occurrence_namespace(role, declares)
1563    }
1564
1565    fn embedded_leaf_facts(
1566        &self,
1567        node: Node<'_>,
1568        kind: NormalizedKind,
1569        source: &str,
1570        cancellation: Option<&CancellationToken>,
1571    ) -> Vec<EmbeddedLeafFact> {
1572        if kind == NormalizedKind::Module {
1573            return crate::regex::module_assigned_facts(node, source);
1574        }
1575        if kind == NormalizedKind::Call {
1576            return crate::regex::embedded_facts(node, source);
1577        }
1578        if kind != NormalizedKind::StringLiteral {
1579            return Vec::new();
1580        }
1581
1582        let mut facts = crate::regex::embedded_facts(node, source);
1583        if node.kind() != "string" || !python_node_is_in_annotation(node) {
1584            return facts;
1585        }
1586
1587        facts.extend(
1588            python_deferred_annotation_identifier_ranges(node, source, cancellation)
1589                .unwrap_or_default()
1590                .into_iter()
1591                .map(|range| EmbeddedLeafFact {
1592                    kind: NormalizedKind::Identifier,
1593                    range,
1594                    parent: None,
1595                    construct: None,
1596                    occurrence_role: Some(OccurrenceRole::TypeOperand),
1597                }),
1598        );
1599        facts
1600    }
1601
1602    fn comparison_operator_spans(
1603        &self,
1604        node: Node<'_>,
1605        step: &mut dyn FnMut() -> bool,
1606    ) -> Option<Vec<Span>> {
1607        if !step() || node.kind() != "comparison_operator" || node.has_error() || node.is_missing()
1608        {
1609            return None;
1610        }
1611        let mut operators = Vec::new();
1612        let mut operands = 0usize;
1613        let complete = visit_python_comparison_children(node, |child, is_operator| {
1614            if !step() || child.is_missing() || child.has_error() {
1615                return false;
1616            }
1617            if is_operator {
1618                if child.start_byte() >= child.end_byte() {
1619                    return false;
1620                }
1621                operators.push(Span {
1622                    start_byte: child.start_byte(),
1623                    end_byte: child.end_byte(),
1624                });
1625            } else if child.is_named() && !child.is_extra() {
1626                operands += 1;
1627            }
1628            true
1629        });
1630        (complete
1631            && !operators.is_empty()
1632            && operands == operators.len().saturating_add(1)
1633            && step())
1634        .then_some(operators)
1635    }
1636
1637    fn extract(&self, node: Node<'_>, kind: NormalizedKind, sink: &mut RoleSink<'_>) {
1638        if let Some(role) = python_occurrence_role(node) {
1639            sink.occurrence_role(node, role);
1640        }
1641        match kind {
1642            NormalizedKind::Call => {
1643                if let Some(function) = node.child_by_field_name("function") {
1644                    // A call's own name is its callee's, so
1645                    // { "kind": "call", "name": "eval" } reads naturally.
1646                    attach_terminal_callee(
1647                        sink,
1648                        function,
1649                        terminal_callee_leaf(function, callee_spine_step),
1650                    );
1651                    if function.kind() == "attribute"
1652                        && let Some(object) = function.child_by_field_name("object")
1653                    {
1654                        attach_role_with_derived_name(
1655                            sink,
1656                            Role::Receiver,
1657                            object,
1658                            expression_name_node,
1659                        );
1660                    }
1661                }
1662                if let Some(arguments) = node.child_by_field_name("arguments") {
1663                    for index in 0..arguments.named_child_count() {
1664                        if !sink.should_continue() {
1665                            break;
1666                        }
1667                        let Some(argument) = arguments.named_child(index) else {
1668                            continue;
1669                        };
1670                        if argument.is_extra() {
1671                            continue;
1672                        }
1673                        match argument.kind() {
1674                            "keyword_argument" => {
1675                                if let (Some(keyword), Some(value)) = (
1676                                    argument.child_by_field_name("name"),
1677                                    argument.child_by_field_name("value"),
1678                                ) {
1679                                    sink.kwarg(keyword, value);
1680                                }
1681                            }
1682                            "list_splat" | "dictionary_splat"
1683                                if python_literal_pack_arguments(argument, sink) => {}
1684                            _ => attach_argument_role_with_derived_name(
1685                                sink,
1686                                argument,
1687                                expression_name_node,
1688                            ),
1689                        }
1690                    }
1691                }
1692            }
1693            NormalizedKind::FieldAccess => {
1694                if let Some(attribute) = node.child_by_field_name("attribute") {
1695                    sink.set_name(attribute);
1696                    sink.role_named(Role::Field, attribute, attribute);
1697                }
1698                if let Some(object) = node.child_by_field_name("object") {
1699                    attach_role_with_derived_name(sink, Role::Object, object, expression_name_node);
1700                }
1701            }
1702            NormalizedKind::Function | NormalizedKind::Method | NormalizedKind::Class => {
1703                if let Some(name) = node.child_by_field_name("name") {
1704                    sink.set_name(name);
1705                }
1706                attach_decorators(sink, node);
1707            }
1708            NormalizedKind::Assignment => {
1709                if let Some(left) = node.child_by_field_name("left") {
1710                    attach_role_with_derived_name(sink, Role::Left, left, expression_name_node);
1711                }
1712                if let Some(right) = node.child_by_field_name("right") {
1713                    attach_role_with_derived_name(sink, Role::Right, right, expression_name_node);
1714                }
1715            }
1716            NormalizedKind::BinaryExpression => {
1717                if let Some(left) = node.child_by_field_name("left") {
1718                    attach_role_with_derived_name(sink, Role::Left, left, expression_name_node);
1719                }
1720                if let Some(right) = node.child_by_field_name("right") {
1721                    attach_role_with_derived_name(sink, Role::Right, right, expression_name_node);
1722                }
1723                if let Some(operator) = node.child_by_field_name("operator") {
1724                    sink.role_named(Role::Operator, operator, operator);
1725                }
1726            }
1727            NormalizedKind::Comparison => {
1728                let shape = python_comparison_shape(node);
1729                if shape.operator_count == 1 && shape.operand_count == 2 {
1730                    if let Some(left) = shape.left {
1731                        attach_role_with_derived_name(sink, Role::Left, left, expression_name_node);
1732                    }
1733                    if let Some(right) = shape.right {
1734                        attach_role_with_derived_name(
1735                            sink,
1736                            Role::Right,
1737                            right,
1738                            expression_name_node,
1739                        );
1740                    }
1741                    if let Some(operator) = shape.operator {
1742                        sink.role_named(Role::Operator, operator, operator);
1743                    }
1744                }
1745            }
1746            NormalizedKind::Assertion => {
1747                if let Some(test) = python_first_named_operand(node) {
1748                    sink.role(Role::Test, test);
1749                }
1750            }
1751            NormalizedKind::If => {
1752                // The grammar names the `if` statement condition, but its
1753                // conditional expression has no fields: its three named
1754                // operands are value, condition, and alternative. Grammar
1755                // extras (`comment`, `line_continuation`) can be interleaved
1756                // between them, so select among the operands (#4797).
1757                let test = if node.kind() == "conditional_expression" {
1758                    Some(
1759                        python_conditional_expression_operands(node)
1760                            .expect("conditional expression operands")[1],
1761                    )
1762                } else {
1763                    node.child_by_field_name("condition")
1764                };
1765                if let Some(test) = test {
1766                    sink.role(Role::Test, test);
1767                }
1768            }
1769            NormalizedKind::Return => {
1770                if let Some(value) = python_first_named_operand(node) {
1771                    sink.role(Role::Value, value);
1772                }
1773            }
1774            NormalizedKind::Throw => {
1775                if let Some(value) = python_first_named_operand(node) {
1776                    sink.role(Role::Value, value);
1777                }
1778            }
1779            NormalizedKind::Import => match node.kind() {
1780                "import_from_statement" => {
1781                    if let Some(module) = node.child_by_field_name("module_name") {
1782                        sink.role_named(Role::Module, module, module);
1783                    }
1784                }
1785                _ => {
1786                    for index in 0..node.named_child_count() {
1787                        if !sink.should_continue() {
1788                            break;
1789                        }
1790                        let Some(child) = node.named_child(index) else {
1791                            continue;
1792                        };
1793                        match child.kind() {
1794                            "dotted_name" => sink.role_named(Role::Module, child, child),
1795                            "aliased_import" => {
1796                                if let Some(name) = child.child_by_field_name("name") {
1797                                    sink.role_named(Role::Module, name, name);
1798                                }
1799                            }
1800                            _ => {}
1801                        }
1802                    }
1803                }
1804            },
1805            NormalizedKind::Identifier | NormalizedKind::Parameter => sink.set_name(node),
1806            NormalizedKind::Decorator => {
1807                if let Some(name) = python_first_named_operand(node).and_then(expression_name_node)
1808                {
1809                    sink.set_name(name);
1810                }
1811            }
1812            NormalizedKind::ForLoop => {
1813                if let Some(right) = node.child_by_field_name("right") {
1814                    attach_role_with_derived_name(
1815                        sink,
1816                        Role::Iterable,
1817                        right,
1818                        expression_name_node,
1819                    );
1820                }
1821            }
1822            NormalizedKind::CollectionLiteral
1823            | NormalizedKind::TupleLiteral
1824            | NormalizedKind::ListLiteral => {
1825                for index in 0..node.named_child_count() {
1826                    let Some(child) = node.named_child(index) else {
1827                        continue;
1828                    };
1829                    if child.is_extra() {
1830                        continue;
1831                    }
1832                    let element = python_unwrap_parenthesized(child);
1833                    attach_role_with_derived_name(
1834                        sink,
1835                        Role::Element,
1836                        element,
1837                        expression_name_node,
1838                    );
1839                }
1840            }
1841            NormalizedKind::ObjectProperty => {
1842                if let Some(key) = node.child_by_field_name("key") {
1843                    sink.role(Role::Key, key);
1844                }
1845                if let Some(value) = node.child_by_field_name("value") {
1846                    sink.role(Role::Value, value);
1847                }
1848            }
1849            NormalizedKind::StringLiteral if node.kind() == "concatenated_string" => {
1850                for index in 0..node.named_child_count() {
1851                    let Some(part) = node.named_child(index) else {
1852                        continue;
1853                    };
1854                    if part.kind() == "string" {
1855                        sink.role(Role::Part, part);
1856                    }
1857                }
1858            }
1859            _ => {}
1860        }
1861    }
1862}
1863
1864#[cfg(test)]
1865mod tests {
1866    use super::*;
1867    use brokk_bifrost_test_macros::hardwon;
1868
1869    fn node_of_kind<'tree>(root: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
1870        let mut pending = vec![root];
1871        while let Some(node) = pending.pop() {
1872            if node.kind() == kind {
1873                return Some(node);
1874            }
1875            let mut cursor = node.walk();
1876            pending.extend(node.named_children(&mut cursor));
1877        }
1878        None
1879    }
1880
1881    fn string_literal_parenthesization(root: Node<'_>) -> (usize, usize) {
1882        let mut pending = vec![root];
1883        let mut string_count = 0;
1884        let mut parenthesized_count = 0;
1885        while let Some(node) = pending.pop() {
1886            if matches!(node.kind(), "string" | "concatenated_string") {
1887                string_count += 1;
1888                if PYTHON_STRUCTURAL_SPEC
1889                    .string_literal_parenthesized(node)
1890                    .expect("Python string-literal parenthesization is supported")
1891                {
1892                    parenthesized_count += 1;
1893                }
1894            }
1895            let mut cursor = node.walk();
1896            pending.extend(node.named_children(&mut cursor));
1897        }
1898        (string_count, parenthesized_count)
1899    }
1900
1901    fn parse_python(source: &str) -> tree_sitter::Tree {
1902        crate::declarations::parse_python_tree(source).expect("Python source tree")
1903    }
1904
1905    #[test]
1906    #[hardwon(
1907        "6b123df7cc68: quicksilver round 6 on brokk-bifrost-python: __all__ replacement retained old exports"
1908    )]
1909    fn qs6_all_replacement_and_extension() {
1910        for (source, expected) in [
1911            ("__all__ = ['old']\n__all__ = ['new']\n", vec!["new"]),
1912            (
1913                "__all__ = ['old']\n__all__ += ['new']\n",
1914                vec!["old", "new"],
1915            ),
1916            ("__all__ = computed\n__all__ = ['new']\n", vec!["new"]),
1917        ] {
1918            let tree = parse_python(source);
1919            let facts = python_source_export_facts(tree.root_node(), source);
1920            assert_eq!(
1921                facts
1922                    .exports
1923                    .iter()
1924                    .map(|e| e.exported_name.as_str())
1925                    .collect::<Vec<_>>(),
1926                expected
1927            );
1928            assert!(facts.complete, "{source}: {facts:?}");
1929        }
1930    }
1931
1932    #[test]
1933    #[hardwon(
1934        "6b123df7cc68: quicksilver round 6 on brokk-bifrost-python: named extras made literal exports and concatenations unreadable"
1935    )]
1936    fn qs6_literal_extras_grammar_boundary() {
1937        let source =
1938            "__all__ = [\n 'first', # comment\n 'second',\n]\nVALUE = ('a' # comment\n 'b')\n";
1939        let tree = parse_python(source);
1940        assert!(!tree.root_node().has_error());
1941        let facts = python_source_export_facts(tree.root_node(), source);
1942        assert!(facts.complete, "{facts:?}");
1943        assert_eq!(
1944            facts
1945                .exports
1946                .iter()
1947                .map(|e| e.exported_name.as_str())
1948                .collect::<Vec<_>>(),
1949            ["first", "second"]
1950        );
1951        let concat = node_of_kind(tree.root_node(), "concatenated_string").unwrap();
1952        assert_eq!(
1953            python_string_key_value(concat, source).as_deref(),
1954            Some("ab")
1955        );
1956    }
1957
1958    #[test]
1959    fn qs6_structural_literal_grammar_matrix() {
1960        // Expected values follow CPython ast literal values; f/named-escape
1961        // strings remain unsupported rather than inventing a decoded value.
1962        for (literal, expected) in [
1963            (r#""{{key}}""#, Some("{{key}}")),
1964            (r#""{key}""#, Some("{key}")),
1965            (r#"f"{{key}}""#, None),
1966            (r#"f"plain""#, None),
1967            (r#"f"{key}""#, None),
1968            (r#""\N{LATIN CAPITAL LETTER A}""#, None),
1969            (r#""A""#, Some("A")),
1970            (
1971                r#""\n{LATIN CAPITAL LETTER A}""#,
1972                Some("\n{LATIN CAPITAL LETTER A}"),
1973            ),
1974        ] {
1975            let source = format!("value = {literal}\n");
1976            let tree = parse_python(&source);
1977            let node = node_of_kind(tree.root_node(), "string").unwrap();
1978            assert_eq!(
1979                python_string_key_value(node, &source).as_deref(),
1980                expected,
1981                "{literal}"
1982            );
1983        }
1984        for (rhs, kind, count, starred) in [
1985            ("[1, 2]", AssignmentSequenceKind::List, 2, false),
1986            ("(1, 2)", AssignmentSequenceKind::Tuple, 2, false),
1987            ("1, 2", AssignmentSequenceKind::Tuple, 2, false),
1988            ("[1, *items]", AssignmentSequenceKind::List, 1, true),
1989            ("(1, *items)", AssignmentSequenceKind::Tuple, 1, true),
1990        ] {
1991            let source = format!("a, *b = {rhs}\n");
1992            let tree = parse_python(&source);
1993            let assignment = node_of_kind(tree.root_node(), "assignment").unwrap();
1994            assert_eq!(
1995                python_assignment_value_shape(assignment.child_by_field_name("right").unwrap()),
1996                Some((kind, count, starred)),
1997                "{rhs}"
1998            );
1999        }
2000        for (value, constant) in [
2001            ("((True))", true),
2002            ("(False)", true),
2003            ("((None))", true),
2004            ("-None", false),
2005            ("-1", true),
2006            ("f'{value}'", false),
2007        ] {
2008            let source = format!("left == {value}\n");
2009            let tree = parse_python(&source);
2010            let comparison = node_of_kind(tree.root_node(), "comparison_operator").unwrap();
2011            let operand = comparison.named_child(1).unwrap();
2012            assert_eq!(python_comparison_constant(operand), constant, "{value}");
2013        }
2014    }
2015
2016    #[test]
2017    fn qs6_source_export_namespace_shapes() {
2018        for (statement, dynamic) in [
2019            ("", false),
2020            (
2021                "import sys as runtime\nsetattr(runtime.modules[__name__], 'late', 1)\n",
2022                true,
2023            ),
2024            ("setattr(__import__(__name__), 'late', 1)\n", true),
2025            (
2026                "import sys as runtime\nalias = runtime.modules[__name__]\nsetattr(alias, 'late', 1)\n",
2027                true,
2028            ),
2029            ("globals()['late'] = 1\n", true),
2030        ] {
2031            let source = format!("__all__ = ['late']\n{statement}");
2032            let tree = parse_python(&source);
2033            let facts = python_source_export_facts(tree.root_node(), &source);
2034            assert_eq!(facts.definitions_complete, !dynamic, "{source}");
2035            assert!(
2036                matches!(
2037                    facts.exports[0].local_definition,
2038                    SourceExportLocalDefinition::Unknown
2039                ) == dynamic,
2040                "{facts:?}"
2041            );
2042            if !dynamic {
2043                assert!(matches!(
2044                    facts.exports[0].local_definition,
2045                    SourceExportLocalDefinition::Undefined
2046                ));
2047            }
2048        }
2049        let source = "__all__ =\n";
2050        let tree = parse_python(source);
2051        assert!(!python_source_export_facts(tree.root_node(), source).definitions_complete);
2052    }
2053
2054    #[test]
2055    fn qs6_binding_kind_grammar_inventory() {
2056        let source = "def run(parameter):\n scalar = 1\n left, right = (1, 2)\n for loop in ():\n  pass\n values = [comp for comp in ()]\n";
2057        let tree = parse_python(source);
2058        let mut pending = vec![tree.root_node()];
2059        let mut found = Vec::new();
2060        while let Some(node) = pending.pop() {
2061            if node.kind() == "identifier" {
2062                let text = node_source_text(node, source);
2063                if text == "comp" && node.parent().unwrap().kind() != "for_in_clause" {
2064                    continue;
2065                }
2066                if let Some(expected) = match text {
2067                    "parameter" => Some(BindingKind::Parameter),
2068                    "scalar" | "left" | "right" => Some(BindingKind::Local),
2069                    "loop" | "comp" => Some(BindingKind::LoopVariable),
2070                    _ => None,
2071                } {
2072                    let activation =
2073                        python_binding_activation(node, node_range(tree.root_node())).unwrap();
2074                    assert_eq!(activation.kind, expected, "{text}: {}", node.to_sexp());
2075                    found.push(text);
2076                }
2077            }
2078            let mut cursor = node.walk();
2079            pending.extend(node.named_children(&mut cursor));
2080        }
2081        assert_eq!(found.len(), 6);
2082    }
2083
2084    #[test]
2085    fn missing_python_all_value_is_incomplete_after_parser_recovery() {
2086        let source = "__all__ =\n";
2087        let mut parser = tree_sitter::Parser::new();
2088        parser
2089            .set_language(&tree_sitter_python::LANGUAGE.into())
2090            .expect("Python parser");
2091        let tree = parser.parse(source, None).expect("Python source tree");
2092        let root = tree.root_node();
2093        let facts = python_source_export_facts(root, source);
2094
2095        assert!(facts.declared, "recovered AST: {}", root.to_sexp());
2096        assert!(!facts.complete, "recovered AST: {}", root.to_sexp());
2097    }
2098
2099    #[test]
2100    fn parenthesized_string_literals_allow_comment_children() {
2101        for (case, source) in [
2102            (
2103                "one comment between concatenated strings",
2104                "VALUE = (\n    r\"\\.\"  # dot\n    r\"(?!-)\"\n)\n",
2105            ),
2106            (
2107                "several comments between concatenated strings",
2108                "VALUE = (\n    r\"a\"  # first\n    # between\n    r\"b\"  # second\n    # trailing\n)\n",
2109            ),
2110            (
2111                "comment before the string",
2112                "VALUE = (\n    # starts below\n    r\"value\"\n)\n",
2113            ),
2114            (
2115                "nested parentheses with a comment",
2116                "VALUE = (\n    (\n        # nested\n        r\"value\"  # closes inner value\n    )\n)\n",
2117            ),
2118        ] {
2119            let tree = parse_python(source);
2120            let root = tree.root_node();
2121            assert!(!root.has_error(), "{case}: {}", root.to_sexp());
2122            let (string_count, parenthesized_count) = string_literal_parenthesization(root);
2123            assert!(string_count > 0, "{case}: {}", root.to_sexp());
2124            assert!(parenthesized_count > 0, "{case}: {}", root.to_sexp());
2125        }
2126
2127        let tree = parse_python("VALUE = (\n    # comment\n    1  # trailing comment\n)\n");
2128        let mut pending = vec![tree.root_node()];
2129        let parenthesized = loop {
2130            let node = pending.pop().expect("parenthesized expression exists");
2131            if node.kind() == "parenthesized_expression" {
2132                break node;
2133            }
2134            let mut cursor = node.walk();
2135            pending.extend(node.named_children(&mut cursor));
2136        };
2137        assert!(python_comparison_constant(parenthesized));
2138        assert_eq!(python_unwrap_parenthesized(parenthesized).kind(), "integer");
2139    }
2140
2141    #[test]
2142    #[hardwon(
2143        "#4797: a backslash continuation before `else` counted as a fourth conditional-expression operand"
2144    )]
2145    fn conditional_expression_selects_condition_past_extras() {
2146        for (case, source) in [
2147            (
2148                "line continuation",
2149                "def f(a, b):\n    return a if b \\\n        else c\n",
2150            ),
2151            (
2152                "comment",
2153                "def f(a, b, c):\n    return (a if b\n            # why not\n            else c)\n",
2154            ),
2155            (
2156                "plain",
2157                "def f(a, b, c):\n    return (a if b\n            else c)\n",
2158            ),
2159        ] {
2160            let tree = parse_python(source);
2161            let root = tree.root_node();
2162            assert!(!root.has_error(), "{case}: {}", root.to_sexp());
2163            let conditional = node_of_kind(root, "conditional_expression").unwrap_or_else(|| {
2164                panic!("{case}: no conditional expression in {}", root.to_sexp())
2165            });
2166            let test = python_conditional_expression_operands(conditional)
2167                .expect("conditional expression operands")[1];
2168            assert_eq!(test.kind(), "identifier", "{case}");
2169            assert_eq!(
2170                test.utf8_text(source.as_bytes()).unwrap(),
2171                "b",
2172                "{case}: {}",
2173                root.to_sexp()
2174            );
2175        }
2176    }
2177
2178    #[test]
2179    #[hardwon(
2180        "#4797: a line continuation inside parentheses hid the sole operand from the parenthesization check"
2181    )]
2182    fn parenthesized_expression_skips_line_continuation() {
2183        let source = "VALUE = (\n    \"value\"\\\n)\n";
2184        let tree = parse_python(source);
2185        let root = tree.root_node();
2186        assert!(!root.has_error(), "{}", root.to_sexp());
2187        let string = node_of_kind(root, "string").expect("string literal");
2188        assert_eq!(
2189            PYTHON_STRUCTURAL_SPEC.string_literal_parenthesized(string),
2190            Some(true),
2191            "{}",
2192            root.to_sexp()
2193        );
2194    }
2195
2196    #[test]
2197    #[hardwon("#4797: a line continuation in a dict literal disabled duplicate-key detection")]
2198    fn duplicate_dictionary_key_skips_extras() {
2199        let source = "D = {1: 2, \\\n 1: 3}\n";
2200        let tree = parse_python(source);
2201        let root = tree.root_node();
2202        assert!(!root.has_error(), "{}", root.to_sexp());
2203        let dictionary = node_of_kind(root, "dictionary").expect("dictionary literal");
2204        let mut cursor = dictionary.walk();
2205        let pairs: Vec<Node<'_>> = dictionary
2206            .named_children(&mut cursor)
2207            .filter(|child| child.kind() == "pair")
2208            .collect();
2209        assert_eq!(pairs.len(), 2, "{}", root.to_sexp());
2210        assert_eq!(
2211            PYTHON_STRUCTURAL_SPEC.duplicate_dictionary_key(pairs[1], source),
2212            Some(true),
2213            "{}",
2214            root.to_sexp()
2215        );
2216    }
2217
2218    #[test]
2219    fn django_domain_validator_strings_parse_without_parenthesization_panics() {
2220        let source = r#"
2221class DomainNameValidator(RegexValidator):
2222    message = _("Enter a valid domain name.")
2223    ul = "\u00a1-\uffff"  # Unicode letters range (must not be a raw string).
2224    # Host patterns.
2225    hostname_re = (
2226        r"[a-z" + ul + r"0-9](?:[a-z" + ul + r"0-9-]{0,61}[a-z" + ul + r"0-9])?"
2227    )
2228    # Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1.
2229    domain_re = r"(?:\.(?!-)[a-z" + ul + r"0-9-]{1,63}(?<!-))*"
2230    # Top-level domain.
2231    tld_no_fqdn_re = (
2232        r"\."  # dot
2233        r"(?!-)"  # can't start with a dash
2234        r"(?:[a-z" + ul + "-]{2,63}"  # domain label
2235        r"|xn--[a-z0-9]{1,59})"  # or punycode label
2236        r"(?<!-)"  # can't end with a dash
2237    )
2238    tld_re = tld_no_fqdn_re + r"\.?"
2239    ascii_only_hostname_re = r"[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?"
2240    ascii_only_domain_re = r"(?:\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-))*"
2241    ascii_only_tld_re = (
2242        r"\."  # dot
2243        r"(?!-)"  # can't start with a dash
2244        r"(?:[a-zA-Z0-9-]{2,63})"  # domain label
2245        r"(?<!-)"  # can't end with a dash
2246        r"\.?"  # may have a trailing dot
2247    )
2248"#;
2249        let tree = parse_python(source);
2250        let root = tree.root_node();
2251        assert!(!root.has_error(), "{}", root.to_sexp());
2252        let (string_count, parenthesized_count) = string_literal_parenthesization(root);
2253        assert!(string_count > 0, "{}", root.to_sexp());
2254        assert!(parenthesized_count > 0, "{}", root.to_sexp());
2255    }
2256
2257    #[test]
2258    fn decoded_string_values_match_python_escape_and_concatenation_semantics() {
2259        for (source, expected) in [
2260            (
2261                r#"MIDDLEWARE = "\x64jango.middleware.csrf.CsrfViewMiddleware""#,
2262                Some("django.middleware.csrf.CsrfViewMiddleware"),
2263            ),
2264            (
2265                r#"MIDDLEWARE = "django.middleware.csrf." "CsrfViewMiddleware""#,
2266                Some("django.middleware.csrf.CsrfViewMiddleware"),
2267            ),
2268            (
2269                r#"MIDDLEWARE = b"django.middleware.csrf.CsrfViewMiddleware""#,
2270                None,
2271            ),
2272        ] {
2273            let mut parser = tree_sitter::Parser::new();
2274            parser
2275                .set_language(&tree_sitter_python::LANGUAGE.into())
2276                .expect("Python parser");
2277            let tree = parser.parse(source, None).expect("Python source tree");
2278            let mut pending = vec![tree.root_node()];
2279            let assignment = loop {
2280                let node = pending.pop().expect("Python tree has an assignment");
2281                if node.kind() == "assignment" {
2282                    break node;
2283                }
2284                let mut cursor = node.walk();
2285                pending.extend(node.named_children(&mut cursor));
2286            };
2287            let value = assignment
2288                .child_by_field_name("right")
2289                .expect("assignment value");
2290            assert_eq!(
2291                python_string_key_value(value, source).as_deref(),
2292                expected,
2293                "{}",
2294                tree.root_node().to_sexp()
2295            );
2296        }
2297    }
2298    #[test]
2299    fn qs6_flat_import_paths_follow_grammar_fields_and_byte_ranges() {
2300        let source = "# \u{e9}\nimport alpha.beta.gamma as alias\nfrom alpha.beta import gamma\nalpha.beta\n";
2301        let tree = parse_python(source);
2302        let root = tree.root_node();
2303        assert!(!root.has_error(), "{}", root.to_sexp());
2304        let import = root.named_child(1).unwrap();
2305        assert_eq!(import.kind(), "import_statement");
2306        let aliased = import.child_by_field_name("name").unwrap();
2307        assert_eq!(aliased.kind(), "aliased_import");
2308        let dotted = aliased.child_by_field_name("name").unwrap();
2309        assert_eq!(dotted.kind(), "dotted_name");
2310        let tokens = PYTHON_STRUCTURAL_SPEC.path_segment_tokens(dotted);
2311        let start = source.find("alpha").unwrap();
2312        assert_eq!(
2313            tokens
2314                .iter()
2315                .map(|node| (node.kind(), node.byte_range()))
2316                .collect::<Vec<_>>(),
2317            vec![
2318                ("identifier", start..start + 5),
2319                ("identifier", start + 6..start + 10),
2320                ("identifier", start + 11..start + 16)
2321            ]
2322        );
2323        for token in tokens {
2324            assert_eq!(
2325                PYTHON_STRUCTURAL_SPEC.qualified_path_root(token),
2326                Some(dotted)
2327            );
2328        }
2329        let alias = aliased.child_by_field_name("alias").unwrap();
2330        assert_eq!(node_source_text(alias, source), "alias");
2331        assert!(PYTHON_STRUCTURAL_SPEC.qualified_path_root(alias).is_none());
2332        let from = root.named_child(2).unwrap();
2333        let module = from.child_by_field_name("module_name").unwrap();
2334        assert_eq!(module.kind(), "dotted_name");
2335        assert_eq!(
2336            PYTHON_STRUCTURAL_SPEC
2337                .path_segment_tokens(module)
2338                .iter()
2339                .map(|n| node_source_text(*n, source))
2340                .collect::<Vec<_>>(),
2341            vec!["alpha", "beta"]
2342        );
2343        let attribute = root.named_child(3).unwrap().named_child(0).unwrap();
2344        assert_eq!(attribute.kind(), "attribute");
2345        assert!(
2346            PYTHON_STRUCTURAL_SPEC
2347                .path_segment_tokens(attribute)
2348                .is_empty()
2349        );
2350        assert!(
2351            PYTHON_STRUCTURAL_SPEC
2352                .qualified_path_root(attribute.child_by_field_name("attribute").unwrap())
2353                .is_none()
2354        );
2355    }
2356
2357    #[test]
2358    fn qs6_syntactic_reexports_obey_redundant_aliases_and_all_membership() {
2359        for (all, listed, ordinary) in [
2360            ("", Some(RouteHopKind::Import), Some(RouteHopKind::Import)),
2361            (
2362                "__all__ = ['z']\n",
2363                Some(RouteHopKind::ReExport),
2364                Some(RouteHopKind::Import),
2365            ),
2366            ("__all__ = dynamic()\n", None, None),
2367        ] {
2368            let source = format!(
2369                "# \u{e9}\nimport a.b\nimport x as x\nfrom x import y as y\nfrom x import y as z\nfrom x import *\n{all}"
2370            );
2371            let tree = parse_python(&source);
2372            let root = tree.root_node();
2373            assert!(!root.has_error(), "{}", root.to_sexp());
2374            let surface = PYTHON_STRUCTURAL_SPEC.curated_export_surface(root, &source);
2375            for (index, field, expected) in [
2376                (1, "name", ordinary),
2377                (2, "name", Some(RouteHopKind::ReExport)),
2378                (3, "name", Some(RouteHopKind::ReExport)),
2379                (4, "name", listed),
2380                (5, "module_name", Some(RouteHopKind::ReExport)),
2381            ] {
2382                let clause = root
2383                    .named_child(index)
2384                    .unwrap()
2385                    .child_by_field_name(field)
2386                    .unwrap();
2387                let name = if clause.kind() == "aliased_import" {
2388                    clause.child_by_field_name("name").unwrap()
2389                } else {
2390                    clause
2391                };
2392                assert_eq!(name.kind(), "dotted_name");
2393                let token = name.named_child(0).unwrap();
2394                assert_eq!(token.kind(), "identifier");
2395                assert_eq!(
2396                    PYTHON_STRUCTURAL_SPEC.indirection_relation(token, &source, &surface),
2397                    expected,
2398                    "{}",
2399                    clause.to_sexp()
2400                );
2401            }
2402        }
2403    }
2404}