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