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