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    visit_python_comparison_children(node, |child, is_operator| {
517        if is_operator {
518            operator_count += 1;
519            operator.get_or_insert(child);
520        } else if child.is_named() && !child.is_extra() {
521            operand_count += 1;
522            if left.is_none() {
523                left = Some(child);
524            } else if right.is_none() {
525                right = Some(child);
526            }
527        }
528        true
529    });
530    debug_assert!(
531        node.has_error() || operand_count == operator_count.saturating_add(1),
532        "well-formed Python comparison must have one more operand than operators: {node:?}"
533    );
534    PythonComparisonShape {
535        operator_count,
536        operand_count,
537        left,
538        right,
539        operator,
540    }
541}
542
543/// Shared grammar-field traversal for singular roles and transient domains.
544fn visit_python_comparison_children<'tree>(
545    node: Node<'tree>,
546    mut visit: impl FnMut(Node<'tree>, bool) -> bool,
547) -> bool {
548    let mut cursor = node.walk();
549    if cursor.goto_first_child() {
550        loop {
551            if !visit(cursor.node(), cursor.field_name() == Some("operators")) {
552                return false;
553            }
554            if !cursor.goto_next_sibling() {
555                break;
556            }
557        }
558    }
559    true
560}
561
562/// Whether a wrapper is a constant form used by comparison-of-constants:
563/// parenthesized scalar literals and unary signs over numeric literals. This
564/// deliberately does not fold arbitrary expressions such as `1 + 2`.
565fn python_comparison_constant(node: Node<'_>) -> bool {
566    let mut current = node;
567    let mut signed = false;
568    loop {
569        match current.kind() {
570            "parenthesized_expression" => {
571                let Some(expression) = python_parenthesized_expression_child(current) else {
572                    return false;
573                };
574                current = expression;
575            }
576            "unary_operator" => {
577                if !current
578                    .child_by_field_name("operator")
579                    .is_some_and(|operator| matches!(operator.kind(), "+" | "-"))
580                {
581                    return false;
582                }
583                let Some(argument) = current.child_by_field_name("argument") else {
584                    return false;
585                };
586                signed = true;
587                current = argument;
588            }
589            "integer" | "float" => return true,
590            "true" | "false" | "none" => return !signed,
591            "string" | "concatenated_string" => {
592                return !signed && python_plain_string_constant(current);
593            }
594            _ => return false,
595        }
596    }
597}
598
599/// Accept ordinary string tokens and implicit literal concatenation, but not
600/// interpolated strings. Tree structure, rather than source-text scanning,
601/// distinguishes the interpolation form.
602fn python_plain_string_constant(node: Node<'_>) -> bool {
603    let mut pending = vec![node];
604    let mut saw_string = false;
605    while let Some(current) = pending.pop() {
606        match current.kind() {
607            "string" => {
608                saw_string = true;
609                let mut cursor = current.walk();
610                if current
611                    .named_children(&mut cursor)
612                    .any(|child| child.kind() == "interpolation")
613                {
614                    return false;
615                }
616            }
617            "concatenated_string" => {
618                let mut cursor = current.walk();
619                let mut parts = 0;
620                for child in current.named_children(&mut cursor) {
621                    if child.kind() != "string" {
622                        return false;
623                    }
624                    parts += 1;
625                    pending.push(child);
626                }
627                if parts == 0 {
628                    return false;
629                }
630            }
631            _ => return false,
632        }
633    }
634    saw_string
635}
636
637/// Grammar node-type → normalized kind. Every name here must exist in the
638/// tree-sitter-python grammar; `tests::python_kind_table_matches_grammar`
639/// asserts that, so a grammar bump that renames a node fails loudly.
640pub const PYTHON_KIND_TABLE: &[(&str, NormalizedKind)] = &[
641    ("call", NormalizedKind::Call),
642    ("attribute", NormalizedKind::FieldAccess),
643    ("function_definition", NormalizedKind::Function),
644    ("lambda", NormalizedKind::Lambda),
645    ("await", NormalizedKind::Await),
646    ("yield", NormalizedKind::Yield),
647    ("module", NormalizedKind::Module),
648    ("class_definition", NormalizedKind::Class),
649    ("assignment", NormalizedKind::Assignment),
650    ("binary_operator", NormalizedKind::BinaryExpression),
651    ("boolean_operator", NormalizedKind::BinaryExpression),
652    ("comparison_operator", NormalizedKind::Comparison),
653    ("assert_statement", NormalizedKind::Assertion),
654    ("conditional_expression", NormalizedKind::If),
655    // These wrappers are emitted as literals only for supported constant
656    // forms; `should_extract` rejects computed or interpolated expressions.
657    ("parenthesized_expression", NormalizedKind::Literal),
658    ("unary_operator", NormalizedKind::Literal),
659    // The implicit `__exit__` of a `with` statement runs at the construct's
660    // own exit, so the statement is the source fact the resource-lifecycle
661    // selector names for that release.
662    ("with_statement", NormalizedKind::ResourceRelease),
663    ("import_statement", NormalizedKind::Import),
664    ("import_from_statement", NormalizedKind::Import),
665    ("identifier", NormalizedKind::Identifier),
666    ("string", NormalizedKind::StringLiteral),
667    ("concatenated_string", NormalizedKind::StringLiteral),
668    ("integer", NormalizedKind::NumericLiteral),
669    ("float", NormalizedKind::NumericLiteral),
670    ("true", NormalizedKind::BooleanLiteral),
671    ("false", NormalizedKind::BooleanLiteral),
672    ("none", NormalizedKind::NullLiteral),
673    ("return_statement", NormalizedKind::Return),
674    ("raise_statement", NormalizedKind::Throw),
675    ("except_clause", NormalizedKind::Catch),
676    ("if_statement", NormalizedKind::If),
677    ("for_statement", NormalizedKind::ForLoop),
678    ("list", NormalizedKind::ListLiteral),
679    ("set", NormalizedKind::CollectionLiteral),
680    ("dictionary", NormalizedKind::CollectionLiteral),
681    ("tuple", NormalizedKind::TupleLiteral),
682    ("pair", NormalizedKind::ObjectProperty),
683    ("while_statement", NormalizedKind::WhileLoop),
684    // Python's indented suite.
685    ("block", NormalizedKind::Block),
686    ("decorator", NormalizedKind::Decorator),
687    ("comment", NormalizedKind::Comment),
688];
689
690/// Attach `decorators` edges for a definition wrapped in Python's
691/// `decorated_definition` node (which itself is not normalized).
692fn attach_decorators(sink: &mut RoleSink<'_>, definition: Node<'_>) {
693    let Some(parent) = definition.parent() else {
694        return;
695    };
696    if parent.kind() != "decorated_definition" {
697        return;
698    }
699    for index in 0..parent.named_child_count() {
700        let Some(child) = parent.named_child(index) else {
701            continue;
702        };
703        if child.kind() == "decorator" {
704            attach_role_with_derived_name(sink, Role::Decorator, child, expression_name_node);
705        }
706    }
707}
708
709static PYTHON_OCCURRENCE_ROLE_SUPPORT: OccurrenceRoleSupport = OccurrenceRoleSupport::NONE
710    .supported(OccurrenceRole::DeclarationName)
711    .supported(OccurrenceRole::Binder)
712    .supported(OccurrenceRole::LabelOrKey)
713    .supported(OccurrenceRole::TypeOperand)
714    .supported(OccurrenceRole::PathSegment)
715    .supported(OccurrenceRole::ImportAlias)
716    .supported(OccurrenceRole::ImportTarget)
717    .supported(OccurrenceRole::ReceiverPosition)
718    .supported(OccurrenceRole::MemberPosition)
719    .supported(OccurrenceRole::ValueReference)
720    .supported(OccurrenceRole::GlobalDirective)
721    .supported(OccurrenceRole::NonlocalDirective);
722
723/// Whether a `dotted_name` names an imported module rather than an ordinary
724/// attribute chain, which decides whether its tail is an import target.
725///
726/// `relative_import` is the wrapper the grammar puts around the module name of
727/// `from .impl import x`. It is the same import target as the `pkg.impl` of
728/// `from pkg.impl import x`, so it passes through like the other wrappers.
729fn python_dotted_name_is_import(dotted_name: Node<'_>) -> bool {
730    let mut current = dotted_name;
731    loop {
732        let Some(parent) = current.parent() else {
733            return false;
734        };
735        match parent.kind() {
736            "import_statement" | "import_from_statement" | "future_import_statement" => {
737                return true;
738            }
739            "aliased_import" | "dotted_name" | "relative_import" => current = parent,
740            _ => return false,
741        }
742    }
743}
744
745/// Classify one Python identifier token by its AST position.
746///
747/// Python has no separate type-identifier node, so annotation operands are
748/// recognized by their enclosing `type` node — the same field the parser uses
749/// to separate `def f(x: T)`'s binder from its annotation.
750fn python_occurrence_role(node: Node<'_>) -> Option<OccurrenceRole> {
751    if node.kind() != "identifier" {
752        return None;
753    }
754    let parent = node.parent()?;
755    let field = field_name_in_parent(parent, node);
756    let role = match parent.kind() {
757        "global_statement" => OccurrenceRole::GlobalDirective,
758        "nonlocal_statement" => OccurrenceRole::NonlocalDirective,
759        "function_definition" | "class_definition" if field == Some("name") => {
760            OccurrenceRole::DeclarationName
761        }
762        // Every annotation, return type and type-alias operand is wrapped in a
763        // `type` node; `generic_type`/`type_parameter` nest inside one.
764        "type" | "generic_type" | "type_parameter" | "constrained_type" | "union_type" => {
765            OccurrenceRole::TypeOperand
766        }
767        "parameters"
768        | "lambda_parameters"
769        | "typed_parameter"
770        | "list_splat_pattern"
771        | "dictionary_splat_pattern"
772        | "tuple_pattern"
773        | "list_pattern"
774        | "pattern_list"
775        | "as_pattern_target" => OccurrenceRole::Binder,
776        "default_parameter" | "typed_default_parameter" if field == Some("name") => {
777            OccurrenceRole::Binder
778        }
779        "for_statement" | "for_in_clause" if field == Some("left") => OccurrenceRole::Binder,
780        "assignment" if field == Some("left") => OccurrenceRole::Binder,
781        "keyword_argument" if python_keyword_argument_label(node) => OccurrenceRole::LabelOrKey,
782        "attribute" => match field {
783            Some("attribute") => OccurrenceRole::MemberPosition,
784            Some("object") => OccurrenceRole::ReceiverPosition,
785            _ => OccurrenceRole::ValueReference,
786        },
787        "aliased_import" if field == Some("alias") => OccurrenceRole::ImportAlias,
788        "import_from_statement" if field == Some("name") => OccurrenceRole::ImportTarget,
789        "dotted_name" => {
790            let is_tail =
791                parent.named_child(parent.named_child_count().saturating_sub(1)) == Some(node);
792            match (is_tail, python_dotted_name_is_import(parent)) {
793                (true, true) => OccurrenceRole::ImportTarget,
794                (true, false) => OccurrenceRole::ValueReference,
795                (false, _) => OccurrenceRole::PathSegment,
796            }
797        }
798        _ => OccurrenceRole::ValueReference,
799    };
800    Some(role)
801}
802
803/// Whether `node` is the identifier token that binds one formal parameter.
804///
805/// Tree-sitter uses a bare identifier directly under `parameters` or
806/// `lambda_parameters`, and wrapper nodes for annotations, defaults, and
807/// splats. The shared label helper selects the exact name child for each
808/// wrapper so identifiers in annotations/default values do not become
809/// parameter facts.
810fn python_identifier_is_parameter(node: Node<'_>) -> bool {
811    if node.kind() != "identifier" {
812        return false;
813    }
814    let Some(parent) = node.parent() else {
815        return false;
816    };
817    match parent.kind() {
818        "parameters" | "lambda_parameters" => true,
819        "typed_parameter"
820        | "default_parameter"
821        | "typed_default_parameter"
822        | "list_splat_pattern"
823        | "dictionary_splat_pattern"
824        | "keyword_separator" => {
825            crate::declarations::python_parameter_label_node(parent) == Some(node)
826        }
827        _ => false,
828    }
829}
830
831/// Whether a `def` declares a method, that is, whether the suite it sits in
832/// belongs to a `class_definition`.
833///
834/// This reads the parse tree rather than the nearest enclosing normalized kind
835/// because the suite between a class and its methods is itself a normalized
836/// node now (`NormalizedKind::Block`, issue #1474). Walking the concrete
837/// ancestors is also the more direct statement of the rule: a nested `def`
838/// inside a method reaches its enclosing `function_definition` first and stays
839/// a function.
840fn python_definition_is_method(definition: Node<'_>) -> bool {
841    let mut current = definition;
842    while let Some(parent) = current.parent() {
843        match parent.kind() {
844            "class_definition" => return true,
845            // The suite that holds the members, and the wrapper a decorated
846            // definition sits in, are pass-through on the way to the owner.
847            "block" | "decorated_definition" => current = parent,
848            _ => return false,
849        }
850    }
851    false
852}
853
854/// The binding one Python binder token introduces, and the interval it is in
855/// effect over.
856///
857/// Python's function locals are scope-categorical rather than positional: a
858/// name assigned anywhere in a function body is a local of that function for
859/// the whole body, which is why a read above the assignment is an
860/// `UnboundLocalError` rather than a read of an outer name. That is exactly
861/// `ScopeWide`. The one positional exception is a comprehension target, which
862/// lives in the comprehension's own implicit scope; the same exception
863/// `analyzer::python::bindings` records as `PythonComprehensionBinding`.
864fn python_binding_activation(binder: Node<'_>, scope: Range) -> Option<BindingActivation> {
865    // A binder outside every form below is an ordinary assignment target --
866    // the members of a module-level or block-level `a, b = ...` pattern list,
867    // for example -- and Python's scope-categorical rule makes it a local of
868    // its declaring scope for the whole scope. Answering `None` here made the
869    // whole file's lexical environment incomplete as soon as one such binder
870    // existed (scripts/test-cost/greedy.py's `s, k = heapq.heappop(heap)` at
871    // module scope), which failed every code-smells gate that derived it.
872    let Some(form) = nearest_ancestor(binder, |kind| {
873        matches!(
874            kind,
875            "parameters"
876                | "lambda_parameters"
877                | "assignment"
878                | "for_statement"
879                | "for_in_clause"
880                | "as_pattern"
881                | "list_comprehension"
882                | "set_comprehension"
883                | "dictionary_comprehension"
884                | "generator_expression"
885        )
886    }) else {
887        return Some(BindingActivation {
888            kind: BindingKind::Local,
889            hoisting: HoistingClass::ScopeWide,
890            activation: scope,
891        });
892    };
893    match form.kind() {
894        "parameters" | "lambda_parameters" => Some(BindingActivation {
895            kind: BindingKind::Parameter,
896            hoisting: HoistingClass::ScopeWide,
897            activation: scope,
898        }),
899        "assignment" => Some(BindingActivation {
900            kind: BindingKind::Local,
901            hoisting: HoistingClass::ScopeWide,
902            activation: scope,
903        }),
904        "for_in_clause" => {
905            // A comprehension clause binds only inside the comprehension.
906            let comprehension = nearest_ancestor(form, |kind| {
907                matches!(
908                    kind,
909                    "list_comprehension"
910                        | "set_comprehension"
911                        | "dictionary_comprehension"
912                        | "generator_expression"
913                )
914            })?;
915            Some(BindingActivation {
916                kind: BindingKind::LoopVariable,
917                hoisting: HoistingClass::DeclaredHead,
918                activation: node_range(comprehension),
919            })
920        }
921        "for_statement" => Some(BindingActivation {
922            kind: BindingKind::LoopVariable,
923            hoisting: HoistingClass::ScopeWide,
924            activation: scope,
925        }),
926        "as_pattern" => Some(BindingActivation {
927            kind: BindingKind::PatternBinder,
928            hoisting: HoistingClass::ScopeWide,
929            activation: scope,
930        }),
931        _ => Some(BindingActivation {
932            kind: BindingKind::Local,
933            hoisting: HoistingClass::ScopeWide,
934            activation: scope,
935        }),
936    }
937}
938
939/// The text one plain string literal denotes, or `None` when the literal is
940/// not plain: an f-string interpolation, an escape sequence this reader does
941/// not decode, or an implicit concatenation. `None` is never an empty name --
942/// it is "this value is computed", which makes the whole surface unreadable.
943/// Whether every member of a curated surface was read from the parse tree.
944#[derive(Debug, Clone, Copy, PartialEq, Eq)]
945enum ReadableSurface {
946    Yes,
947    No,
948}
949
950#[derive(Debug, Clone)]
951struct PythonExportMember {
952    name: String,
953    range: Range,
954}
955
956/// Collect the members of an `__all__` value into `names`, reporting whether
957/// every member was readable. Only a list or tuple display of plain string
958/// literals is; anything else is a value the source computes.
959fn python_collect_all_members(
960    value: Node<'_>,
961    source: &str,
962    members: &mut Vec<PythonExportMember>,
963) -> ReadableSurface {
964    if !matches!(value.kind(), "list" | "tuple") {
965        return ReadableSurface::No;
966    }
967    let mut cursor = value.walk();
968    for element in value.named_children(&mut cursor) {
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 => return ReadableSurface::No,
977        }
978    }
979    ReadableSurface::Yes
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                    let extends = expression.kind() == "assignment"
1175                        || expression
1176                            .child_by_field_name("operator")
1177                            .is_some_and(|operator| operator.kind() == "+=");
1178                    if !extends
1179                        || python_collect_all_members(value, source, &mut members)
1180                            == ReadableSurface::No
1181                    {
1182                        readable = ReadableSurface::No;
1183                    }
1184                }
1185                "call" => {
1186                    let Some(function) = expression.child_by_field_name("function") else {
1187                        continue;
1188                    };
1189                    if function.kind() == "attribute"
1190                        && names_all(function.child_by_field_name("object"))
1191                    {
1192                        stated = true;
1193                        readable = ReadableSurface::No;
1194                    }
1195                }
1196                _ => {}
1197            }
1198        }
1199    }
1200
1201    let mut names = HashSet::default();
1202    let exports = if stated {
1203        members
1204            .into_iter()
1205            .filter(|member| names.insert(member.name.clone()))
1206            .map(|member| {
1207                let local_definition = match module_bindings.get(member.name.as_str()) {
1208                    Some(binding) => SourceExportLocalDefinition::Defined {
1209                        binding: binding.clone(),
1210                    },
1211                    None if definitions_complete => SourceExportLocalDefinition::Undefined,
1212                    None => SourceExportLocalDefinition::Unknown,
1213                };
1214                SourceExportFact {
1215                    range: member.range,
1216                    exported_name: member.name,
1217                    local_definition,
1218                }
1219            })
1220            .collect()
1221    } else {
1222        Vec::new()
1223    };
1224    SourceExportFacts {
1225        exports,
1226        declared: stated,
1227        complete: !stated || readable == ReadableSurface::Yes,
1228        definitions_complete,
1229    }
1230}
1231
1232/// The names a Python module curates as its public surface: the value of its
1233/// module-level `__all__`.
1234///
1235/// Only the module's own statements are read. An `__all__` inside a function
1236/// is a local rather than the module's surface, and a name the module binds
1237/// conditionally is still bound by the statement this reader already sees.
1238/// A statement that assigns, extends, or mutates `__all__` with anything but a
1239/// list or tuple of plain string literals makes the surface unreadable: the
1240/// members are then unknown, and no import is classified from them.
1241fn python_curated_export_surface(root: Node<'_>, source: &str) -> CuratedExportSurface {
1242    let facts = python_source_export_facts(root, source);
1243    match (facts.declared, facts.complete) {
1244        (false, _) => CuratedExportSurface::Absent,
1245        (true, false) => CuratedExportSurface::Unreadable,
1246        (true, true) => CuratedExportSurface::Listed(
1247            facts
1248                .exports
1249                .into_iter()
1250                .map(|export| export.exported_name)
1251                .collect(),
1252        ),
1253    }
1254}
1255
1256/// Which indirection relation one Python import token participates in.
1257///
1258/// Python's grammar names no re-export, so the relation follows the explicit
1259/// re-export rules the typing ecosystem already enforces (PEP 484 stub
1260/// semantics, applied by pyright and mypy in strict mode), which makes this
1261/// relation agree with what a type checker calls public:
1262///
1263/// 1. A name on the module's `__all__` is a re-export of whatever binding the
1264///    module gives that name.
1265/// 2. The redundant-alias forms `from x import y as y` and `import x as x`
1266///    are re-exports.
1267/// 3. `from x import *` is one star hop that forwards the public surface of
1268///    `x`; the expansion is the import machinery's work, not this producer's,
1269///    so the hop is recorded on the module reference and nothing is
1270///    enumerated here.
1271/// 4. Every other import is an ordinary import, including a plain
1272///    `from .impl import helper` in a package `__init__.py` that states no
1273///    `__all__`. The facade convention alone does not make a name public, and
1274///    a consumer that wants every name a facade imports already has the
1275///    import relation.
1276///
1277/// `None` is the answer for a name whose membership only an unreadable
1278/// `__all__` could settle; the file's relations then report incomplete rather
1279/// than guessing either way.
1280fn python_indirection_relation(
1281    token: Node<'_>,
1282    source: &str,
1283    surface: &CuratedExportSurface,
1284) -> Option<RouteHopKind> {
1285    let statement = nearest_ancestor(token, |kind| {
1286        matches!(
1287            kind,
1288            "import_statement" | "import_from_statement" | "future_import_statement"
1289        )
1290    })?;
1291    // The statement's own child that holds this token: a `module_name` field,
1292    // or one `name` field of the import list.
1293    let mut clause = token;
1294    while let Some(parent) = clause.parent() {
1295        if parent.id() == statement.id() {
1296            break;
1297        }
1298        clause = parent;
1299    }
1300
1301    if field_name_in_parent(statement, clause) == Some("module_name") {
1302        // `from x import a` binds `a`, not `x`, so the module reference
1303        // forwards nothing -- unless the import is the star form, whose one
1304        // hop forwards the whole surface of `x`.
1305        let mut cursor = statement.walk();
1306        let star = statement
1307            .children(&mut cursor)
1308            .any(|child| child.kind() == "wildcard_import");
1309        return Some(if star {
1310            RouteHopKind::ReExport
1311        } else {
1312            RouteHopKind::Import
1313        });
1314    }
1315
1316    let bound = match clause.kind() {
1317        "aliased_import" => {
1318            let name = clause.child_by_field_name("name")?;
1319            let alias = clause.child_by_field_name("alias")?;
1320            if node_source_text(name, source) == node_source_text(alias, source) {
1321                return Some(RouteHopKind::ReExport);
1322            }
1323            alias
1324        }
1325        // `import a.b.c` binds the top package `a`; `from m import a` binds
1326        // the single-segment name the import list spells.
1327        "dotted_name" if statement.kind() == "import_statement" => clause.named_child(0)?,
1328        "dotted_name" => clause,
1329        _ => return None,
1330    };
1331    match surface.lists(node_source_text(bound, source)) {
1332        Some(true) => Some(RouteHopKind::ReExport),
1333        Some(false) => Some(RouteHopKind::Import),
1334        None => None,
1335    }
1336}
1337
1338/// Grammar steps for the shared call-callee spine. A value expression has no step.
1339pub fn callee_spine_step(kind: &str) -> Option<CalleeSpineStep> {
1340    use CalleeSpineStep::{Field, Name};
1341    match kind {
1342        "identifier" => Some(Name),
1343        "attribute" => Some(Field("attribute")),
1344        _ => None,
1345    }
1346}
1347
1348impl StructuralSpec for PythonStructuralSpec {
1349    fn language(&self) -> Language {
1350        Language::Python
1351    }
1352
1353    fn supports_boolean_literal_value(&self) -> bool {
1354        true
1355    }
1356
1357    fn supports_string_literal_parenthesized(&self) -> bool {
1358        true
1359    }
1360
1361    fn supports_string_literal_value(&self) -> bool {
1362        true
1363    }
1364
1365    fn string_literal_value(&self, node: Node<'_>, source: &str) -> Option<String> {
1366        python_string_key_value(node, source)
1367    }
1368
1369    fn supports_assignment_shape(&self) -> bool {
1370        true
1371    }
1372
1373    fn assignment_shape(&self, node: Node<'_>) -> Option<AssignmentShape> {
1374        python_assignment_shape(node)
1375    }
1376
1377    fn supports_integer_literal_value(&self) -> bool {
1378        true
1379    }
1380
1381    fn integer_literal_value(&self, node: Node<'_>, source: &str) -> Option<IntegerLiteralValue> {
1382        python_integer_literal_value(node, source)
1383    }
1384
1385    fn supports_duplicate_dictionary_keys(&self) -> bool {
1386        true
1387    }
1388
1389    fn duplicate_dictionary_key(&self, node: Node<'_>, source: &str) -> Option<bool> {
1390        python_duplicate_dictionary_key(node, source)
1391    }
1392
1393    fn string_literal_parenthesized(&self, node: Node<'_>) -> Option<bool> {
1394        if !matches!(node.kind(), "string" | "concatenated_string") {
1395            return None;
1396        }
1397        let mut current = node;
1398        let mut parenthesized = false;
1399        while let Some(parent) = current.parent() {
1400            if parent.kind() != "parenthesized_expression" {
1401                break;
1402            }
1403            assert_eq!(python_parenthesized_expression_child(parent), Some(current));
1404            current = parent;
1405            parenthesized = true;
1406        }
1407        Some(parenthesized)
1408    }
1409
1410    fn reference_edge_support(&self) -> &ReferenceEdgeSupport {
1411        &DEEP_REFERENCE_EDGE_SUPPORT
1412    }
1413
1414    fn identity_route_support(&self) -> &IdentityRouteSupport {
1415        // `import x as y` is an alias, and `python_indirection_relation`
1416        // states which imports re-export (issue #1649).
1417        static SUPPORT: IdentityRouteSupport = DEEP_IDENTITY_AXES
1418            .supported_relation(RouteHopKind::Alias)
1419            .supported_relation(RouteHopKind::Import)
1420            .supported_relation(RouteHopKind::ReExport)
1421            .supported_relation(RouteHopKind::NestedOwner);
1422        &SUPPORT
1423    }
1424
1425    /// Python's one qualified-path chain is `dotted_name`, which is flat
1426    /// rather than left-nested: its named children are the segments in order.
1427    fn qualified_path_root<'tree>(&self, token: Node<'tree>) -> Option<Node<'tree>> {
1428        if token.kind() != "identifier" {
1429            return None;
1430        }
1431        token
1432            .parent()
1433            .filter(|parent| parent.kind() == "dotted_name")
1434    }
1435
1436    fn path_segment_tokens<'tree>(&self, root: Node<'tree>) -> Vec<Node<'tree>> {
1437        if root.kind() != "dotted_name" {
1438            return Vec::new();
1439        }
1440        let mut cursor = root.walk();
1441        root.named_children(&mut cursor)
1442            .filter(|child| child.kind() == "identifier")
1443            .collect()
1444    }
1445
1446    fn curated_export_surface(&self, root: Node<'_>, source: &str) -> CuratedExportSurface {
1447        python_curated_export_surface(root, source)
1448    }
1449
1450    fn indirection_relation(
1451        &self,
1452        token: Node<'_>,
1453        source: &str,
1454        surface: &CuratedExportSurface,
1455    ) -> Option<RouteHopKind> {
1456        python_indirection_relation(token, source, surface)
1457    }
1458
1459    fn scope_formation(&self, kind: NormalizedKind) -> ScopeFormation {
1460        match kind {
1461            // The normalized module fact is the file root, not a nested scope.
1462            // Top-level imports and assignments belong to the synthetic file scope.
1463            NormalizedKind::Module => ScopeFormation::NotAScope,
1464            // Python blocks do not introduce local-variable scopes. Names
1465            // assigned in an if/loop/try body remain function or module locals.
1466            NormalizedKind::Block | NormalizedKind::ForLoop | NormalizedKind::WhileLoop => {
1467                ScopeFormation::NotAScope
1468            }
1469            _ => default_scope_formation(kind),
1470        }
1471    }
1472
1473    fn kind_table(&self) -> &'static [(&'static str, NormalizedKind)] {
1474        PYTHON_KIND_TABLE
1475    }
1476
1477    fn refine_kind(
1478        &self,
1479        node: Node<'_>,
1480        kind: NormalizedKind,
1481        _enclosing: Option<NormalizedKind>,
1482        _source: &str,
1483        _context: &CallSiteContext,
1484    ) -> NormalizedKind {
1485        match kind {
1486            NormalizedKind::Identifier if python_identifier_is_parameter(node) => {
1487                NormalizedKind::Parameter
1488            }
1489            NormalizedKind::Function if python_definition_is_method(node) => NormalizedKind::Method,
1490            NormalizedKind::Comparison if python_comparison_shape(node).operator_count > 1 => {
1491                NormalizedKind::ComparisonChain
1492            }
1493            _ => kind,
1494        }
1495    }
1496
1497    fn should_extract(&self, node: Node<'_>, kind: NormalizedKind) -> bool {
1498        if kind == NormalizedKind::Module && node.byte_range().is_empty() {
1499            return false;
1500        }
1501        match node.kind() {
1502            "parenthesized_expression" | "unary_operator" => {
1503                kind == NormalizedKind::Literal && python_comparison_constant(node)
1504            }
1505            _ => kind != NormalizedKind::Assignment || node.child_by_field_name("right").is_some(),
1506        }
1507    }
1508
1509    fn supports_kind(&self, kind: NormalizedKind) -> bool {
1510        brokk_bifrost_core::analyzer::structural::spec::supports_kind_from_table(
1511            kind,
1512            &[
1513                NormalizedKind::Method,
1514                // Module facts select the parser's file root for Python policies.
1515                NormalizedKind::Module,
1516                NormalizedKind::Parameter,
1517                NormalizedKind::SyntaxError,
1518                NormalizedKind::ComparisonChain,
1519                NormalizedKind::RegexPattern,
1520                NormalizedKind::RegexSequence,
1521                NormalizedKind::RegexGroup,
1522                NormalizedKind::RegexAlternation,
1523                NormalizedKind::RegexCharacterClass,
1524                NormalizedKind::RegexCharacterRange,
1525                NormalizedKind::RegexQuantifier,
1526                NormalizedKind::RegexAnchor,
1527                NormalizedKind::RegexEscape,
1528                NormalizedKind::RegexLiteral,
1529                NormalizedKind::RegexWildcard,
1530                NormalizedKind::RegexRiskHostname,
1531                NormalizedKind::RegexRiskRange,
1532                NormalizedKind::RegexRiskBadTag,
1533                NormalizedKind::RegexRiskExponential,
1534                NormalizedKind::RegexIncomplete,
1535                NormalizedKind::RegexRiskBackspaceEscape,
1536                NormalizedKind::RegexRiskDuplicateClassCharacter,
1537                NormalizedKind::RegexRiskUnmatchableCaret,
1538                NormalizedKind::RegexRiskUnmatchableDollar,
1539                NormalizedKind::RegexRiskIncompleteSpecialGroup,
1540                NormalizedKind::RegexRiskPolynomial,
1541            ],
1542            self.kind_table(),
1543        )
1544    }
1545
1546    fn occurrence_role_support(&self) -> &OccurrenceRoleSupport {
1547        &PYTHON_OCCURRENCE_ROLE_SUPPORT
1548    }
1549
1550    fn lexical_environment_support(&self) -> &LexicalEnvironmentSupport {
1551        &DEEP_LEXICAL_ENVIRONMENT_SUPPORT
1552    }
1553
1554    fn materialization_support(&self) -> &DeclarationMaterializationSupport {
1555        &PYTHON_MATERIALIZATION_SUPPORT
1556    }
1557
1558    fn source_export_facts(&self, root: Node<'_>, source: &str) -> Option<SourceExportFacts> {
1559        Some(python_source_export_facts(root, source))
1560    }
1561
1562    fn binding_activation(&self, binder: Node<'_>, scope: Range) -> Option<BindingActivation> {
1563        python_binding_activation(binder, scope)
1564    }
1565
1566    /// Python only classifies a scope segment inside a `dotted_name`, and every
1567    /// non-tail segment of a dotted name is a module.
1568    fn occurrence_namespace(
1569        &self,
1570        role: OccurrenceRole,
1571        declares: Option<NormalizedKind>,
1572    ) -> Option<Namespace> {
1573        module_path_occurrence_namespace(role, declares)
1574    }
1575
1576    fn embedded_leaf_facts(
1577        &self,
1578        node: Node<'_>,
1579        kind: NormalizedKind,
1580        source: &str,
1581        cancellation: Option<&CancellationToken>,
1582    ) -> Vec<EmbeddedLeafFact> {
1583        if kind == NormalizedKind::Module {
1584            return crate::regex::module_assigned_facts(node, source);
1585        }
1586        if kind == NormalizedKind::Call {
1587            return crate::regex::embedded_facts(node, source);
1588        }
1589        if kind != NormalizedKind::StringLiteral {
1590            return Vec::new();
1591        }
1592
1593        let mut facts = crate::regex::embedded_facts(node, source);
1594        if node.kind() != "string" || !python_node_is_in_annotation(node) {
1595            return facts;
1596        }
1597
1598        facts.extend(
1599            python_deferred_annotation_identifier_ranges(node, source, cancellation)
1600                .unwrap_or_default()
1601                .into_iter()
1602                .map(|range| EmbeddedLeafFact {
1603                    kind: NormalizedKind::Identifier,
1604                    range,
1605                    parent: None,
1606                    construct: None,
1607                    occurrence_role: Some(OccurrenceRole::TypeOperand),
1608                }),
1609        );
1610        facts
1611    }
1612
1613    fn comparison_operator_spans(
1614        &self,
1615        node: Node<'_>,
1616        step: &mut dyn FnMut() -> bool,
1617    ) -> Option<Vec<Span>> {
1618        if !step() || node.kind() != "comparison_operator" || node.has_error() || node.is_missing()
1619        {
1620            return None;
1621        }
1622        let mut operators = Vec::new();
1623        let mut operands = 0usize;
1624        let complete = visit_python_comparison_children(node, |child, is_operator| {
1625            if !step() || child.is_missing() || child.has_error() {
1626                return false;
1627            }
1628            if is_operator {
1629                if child.start_byte() >= child.end_byte() {
1630                    return false;
1631                }
1632                operators.push(Span {
1633                    start_byte: child.start_byte(),
1634                    end_byte: child.end_byte(),
1635                });
1636            } else if child.is_named() && !child.is_extra() {
1637                operands += 1;
1638            }
1639            true
1640        });
1641        (complete
1642            && !operators.is_empty()
1643            && operands == operators.len().saturating_add(1)
1644            && step())
1645        .then_some(operators)
1646    }
1647
1648    fn extract(&self, node: Node<'_>, kind: NormalizedKind, sink: &mut RoleSink<'_>) {
1649        if let Some(role) = python_occurrence_role(node) {
1650            sink.occurrence_role(node, role);
1651        }
1652        match kind {
1653            NormalizedKind::Call => {
1654                if let Some(function) = node.child_by_field_name("function") {
1655                    // A call's own name is its callee's, so
1656                    // { "kind": "call", "name": "eval" } reads naturally.
1657                    attach_terminal_callee(
1658                        sink,
1659                        function,
1660                        terminal_callee_leaf(function, callee_spine_step),
1661                    );
1662                    if function.kind() == "attribute"
1663                        && let Some(object) = function.child_by_field_name("object")
1664                    {
1665                        attach_role_with_derived_name(
1666                            sink,
1667                            Role::Receiver,
1668                            object,
1669                            expression_name_node,
1670                        );
1671                    }
1672                }
1673                if let Some(arguments) = node.child_by_field_name("arguments") {
1674                    for index in 0..arguments.named_child_count() {
1675                        if !sink.should_continue() {
1676                            break;
1677                        }
1678                        let Some(argument) = arguments.named_child(index) else {
1679                            continue;
1680                        };
1681                        if argument.is_extra() {
1682                            continue;
1683                        }
1684                        match argument.kind() {
1685                            "keyword_argument" => {
1686                                if let (Some(keyword), Some(value)) = (
1687                                    argument.child_by_field_name("name"),
1688                                    argument.child_by_field_name("value"),
1689                                ) {
1690                                    sink.kwarg(keyword, value);
1691                                }
1692                            }
1693                            "list_splat" | "dictionary_splat"
1694                                if python_literal_pack_arguments(argument, sink) => {}
1695                            _ => attach_argument_role_with_derived_name(
1696                                sink,
1697                                argument,
1698                                expression_name_node,
1699                            ),
1700                        }
1701                    }
1702                }
1703            }
1704            NormalizedKind::FieldAccess => {
1705                if let Some(attribute) = node.child_by_field_name("attribute") {
1706                    sink.set_name(attribute);
1707                    sink.role_named(Role::Field, attribute, attribute);
1708                }
1709                if let Some(object) = node.child_by_field_name("object") {
1710                    attach_role_with_derived_name(sink, Role::Object, object, expression_name_node);
1711                }
1712            }
1713            NormalizedKind::Function | NormalizedKind::Method | NormalizedKind::Class => {
1714                if let Some(name) = node.child_by_field_name("name") {
1715                    sink.set_name(name);
1716                }
1717                attach_decorators(sink, node);
1718            }
1719            NormalizedKind::Assignment => {
1720                if let Some(left) = node.child_by_field_name("left") {
1721                    attach_role_with_derived_name(sink, Role::Left, left, expression_name_node);
1722                }
1723                if let Some(right) = node.child_by_field_name("right") {
1724                    attach_role_with_derived_name(sink, Role::Right, right, expression_name_node);
1725                }
1726            }
1727            NormalizedKind::BinaryExpression => {
1728                if let Some(left) = node.child_by_field_name("left") {
1729                    attach_role_with_derived_name(sink, Role::Left, left, expression_name_node);
1730                }
1731                if let Some(right) = node.child_by_field_name("right") {
1732                    attach_role_with_derived_name(sink, Role::Right, right, expression_name_node);
1733                }
1734                if let Some(operator) = node.child_by_field_name("operator") {
1735                    sink.role_named(Role::Operator, operator, operator);
1736                }
1737            }
1738            NormalizedKind::Comparison => {
1739                let shape = python_comparison_shape(node);
1740                if shape.operator_count == 1 && shape.operand_count == 2 {
1741                    if let Some(left) = shape.left {
1742                        attach_role_with_derived_name(sink, Role::Left, left, expression_name_node);
1743                    }
1744                    if let Some(right) = shape.right {
1745                        attach_role_with_derived_name(
1746                            sink,
1747                            Role::Right,
1748                            right,
1749                            expression_name_node,
1750                        );
1751                    }
1752                    if let Some(operator) = shape.operator {
1753                        sink.role_named(Role::Operator, operator, operator);
1754                    }
1755                }
1756            }
1757            NormalizedKind::Assertion => {
1758                if let Some(test) = python_first_named_operand(node) {
1759                    sink.role(Role::Test, test);
1760                }
1761            }
1762            NormalizedKind::If => {
1763                // The grammar names the `if` statement condition, but its
1764                // conditional expression has no fields: its three named
1765                // operands are value, condition, and alternative. Grammar
1766                // extras (`comment`, `line_continuation`) can be interleaved
1767                // between them, so select among the operands (#4797).
1768                let test = if node.kind() == "conditional_expression" {
1769                    Some(python_conditional_expression_test(node))
1770                } else {
1771                    node.child_by_field_name("condition")
1772                };
1773                if let Some(test) = test {
1774                    sink.role(Role::Test, test);
1775                }
1776            }
1777            NormalizedKind::Return => {
1778                if let Some(value) = python_first_named_operand(node) {
1779                    sink.role(Role::Value, value);
1780                }
1781            }
1782            NormalizedKind::Throw => {
1783                if let Some(value) = python_first_named_operand(node) {
1784                    sink.role(Role::Value, value);
1785                }
1786            }
1787            NormalizedKind::Import => match node.kind() {
1788                "import_from_statement" => {
1789                    if let Some(module) = node.child_by_field_name("module_name") {
1790                        sink.role_named(Role::Module, module, module);
1791                    }
1792                }
1793                _ => {
1794                    for index in 0..node.named_child_count() {
1795                        if !sink.should_continue() {
1796                            break;
1797                        }
1798                        let Some(child) = node.named_child(index) else {
1799                            continue;
1800                        };
1801                        match child.kind() {
1802                            "dotted_name" => sink.role_named(Role::Module, child, child),
1803                            "aliased_import" => {
1804                                if let Some(name) = child.child_by_field_name("name") {
1805                                    sink.role_named(Role::Module, name, name);
1806                                }
1807                            }
1808                            _ => {}
1809                        }
1810                    }
1811                }
1812            },
1813            NormalizedKind::Identifier | NormalizedKind::Parameter => sink.set_name(node),
1814            NormalizedKind::Decorator => {
1815                if let Some(name) = python_first_named_operand(node).and_then(expression_name_node)
1816                {
1817                    sink.set_name(name);
1818                }
1819            }
1820            NormalizedKind::ForLoop => {
1821                if let Some(right) = node.child_by_field_name("right") {
1822                    attach_role_with_derived_name(
1823                        sink,
1824                        Role::Iterable,
1825                        right,
1826                        expression_name_node,
1827                    );
1828                }
1829            }
1830            NormalizedKind::CollectionLiteral
1831            | NormalizedKind::TupleLiteral
1832            | NormalizedKind::ListLiteral => {
1833                for index in 0..node.named_child_count() {
1834                    let Some(child) = node.named_child(index) else {
1835                        continue;
1836                    };
1837                    if child.is_extra() {
1838                        continue;
1839                    }
1840                    let element = python_unwrap_parenthesized(child);
1841                    attach_role_with_derived_name(
1842                        sink,
1843                        Role::Element,
1844                        element,
1845                        expression_name_node,
1846                    );
1847                }
1848            }
1849            NormalizedKind::ObjectProperty => {
1850                if let Some(key) = node.child_by_field_name("key") {
1851                    sink.role(Role::Key, key);
1852                }
1853                if let Some(value) = node.child_by_field_name("value") {
1854                    sink.role(Role::Value, value);
1855                }
1856            }
1857            NormalizedKind::StringLiteral if node.kind() == "concatenated_string" => {
1858                for index in 0..node.named_child_count() {
1859                    let Some(part) = node.named_child(index) else {
1860                        continue;
1861                    };
1862                    if part.kind() == "string" {
1863                        sink.role(Role::Part, part);
1864                    }
1865                }
1866            }
1867            _ => {}
1868        }
1869    }
1870}
1871
1872#[cfg(test)]
1873mod tests {
1874    use super::*;
1875    use brokk_bifrost_test_macros::hardwon;
1876
1877    fn node_of_kind<'tree>(root: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
1878        let mut pending = vec![root];
1879        while let Some(node) = pending.pop() {
1880            if node.kind() == kind {
1881                return Some(node);
1882            }
1883            let mut cursor = node.walk();
1884            pending.extend(node.named_children(&mut cursor));
1885        }
1886        None
1887    }
1888
1889    fn string_literal_parenthesization(root: Node<'_>) -> (usize, usize) {
1890        let mut pending = vec![root];
1891        let mut string_count = 0;
1892        let mut parenthesized_count = 0;
1893        while let Some(node) = pending.pop() {
1894            if matches!(node.kind(), "string" | "concatenated_string") {
1895                string_count += 1;
1896                if PYTHON_STRUCTURAL_SPEC
1897                    .string_literal_parenthesized(node)
1898                    .expect("Python string-literal parenthesization is supported")
1899                {
1900                    parenthesized_count += 1;
1901                }
1902            }
1903            let mut cursor = node.walk();
1904            pending.extend(node.named_children(&mut cursor));
1905        }
1906        (string_count, parenthesized_count)
1907    }
1908
1909    fn parse_python(source: &str) -> tree_sitter::Tree {
1910        crate::declarations::parse_python_tree(source).expect("Python source tree")
1911    }
1912
1913    #[test]
1914    fn missing_python_all_value_is_incomplete_after_parser_recovery() {
1915        let source = "__all__ =\n";
1916        let mut parser = tree_sitter::Parser::new();
1917        parser
1918            .set_language(&tree_sitter_python::LANGUAGE.into())
1919            .expect("Python parser");
1920        let tree = parser.parse(source, None).expect("Python source tree");
1921        let root = tree.root_node();
1922        let facts = python_source_export_facts(root, source);
1923
1924        assert!(facts.declared, "recovered AST: {}", root.to_sexp());
1925        assert!(!facts.complete, "recovered AST: {}", root.to_sexp());
1926    }
1927
1928    #[test]
1929    fn parenthesized_string_literals_allow_comment_children() {
1930        for (case, source) in [
1931            (
1932                "one comment between concatenated strings",
1933                "VALUE = (\n    r\"\\.\"  # dot\n    r\"(?!-)\"\n)\n",
1934            ),
1935            (
1936                "several comments between concatenated strings",
1937                "VALUE = (\n    r\"a\"  # first\n    # between\n    r\"b\"  # second\n    # trailing\n)\n",
1938            ),
1939            (
1940                "comment before the string",
1941                "VALUE = (\n    # starts below\n    r\"value\"\n)\n",
1942            ),
1943            (
1944                "nested parentheses with a comment",
1945                "VALUE = (\n    (\n        # nested\n        r\"value\"  # closes inner value\n    )\n)\n",
1946            ),
1947        ] {
1948            let tree = parse_python(source);
1949            let root = tree.root_node();
1950            assert!(!root.has_error(), "{case}: {}", root.to_sexp());
1951            let (string_count, parenthesized_count) = string_literal_parenthesization(root);
1952            assert!(string_count > 0, "{case}: {}", root.to_sexp());
1953            assert!(parenthesized_count > 0, "{case}: {}", root.to_sexp());
1954        }
1955
1956        let tree = parse_python("VALUE = (\n    # comment\n    1  # trailing comment\n)\n");
1957        let mut pending = vec![tree.root_node()];
1958        let parenthesized = loop {
1959            let node = pending.pop().expect("parenthesized expression exists");
1960            if node.kind() == "parenthesized_expression" {
1961                break node;
1962            }
1963            let mut cursor = node.walk();
1964            pending.extend(node.named_children(&mut cursor));
1965        };
1966        assert!(python_comparison_constant(parenthesized));
1967        assert_eq!(python_unwrap_parenthesized(parenthesized).kind(), "integer");
1968    }
1969
1970    #[test]
1971    #[hardwon(
1972        "#4797: a backslash continuation before `else` counted as a fourth conditional-expression operand"
1973    )]
1974    fn conditional_expression_selects_condition_past_extras() {
1975        for (case, source) in [
1976            (
1977                "line continuation",
1978                "def f(a, b):\n    return a if b \\\n        else c\n",
1979            ),
1980            (
1981                "comment",
1982                "def f(a, b, c):\n    return (a if b\n            # why not\n            else c)\n",
1983            ),
1984            (
1985                "plain",
1986                "def f(a, b, c):\n    return (a if b\n            else c)\n",
1987            ),
1988        ] {
1989            let tree = parse_python(source);
1990            let root = tree.root_node();
1991            assert!(!root.has_error(), "{case}: {}", root.to_sexp());
1992            let conditional = node_of_kind(root, "conditional_expression").unwrap_or_else(|| {
1993                panic!("{case}: no conditional expression in {}", root.to_sexp())
1994            });
1995            let test = python_conditional_expression_test(conditional);
1996            assert_eq!(test.kind(), "identifier", "{case}");
1997            assert_eq!(
1998                test.utf8_text(source.as_bytes()).unwrap(),
1999                "b",
2000                "{case}: {}",
2001                root.to_sexp()
2002            );
2003        }
2004    }
2005
2006    #[test]
2007    #[hardwon(
2008        "#4797: a line continuation inside parentheses hid the sole operand from the parenthesization check"
2009    )]
2010    fn parenthesized_expression_skips_line_continuation() {
2011        let source = "VALUE = (\n    \"value\"\\\n)\n";
2012        let tree = parse_python(source);
2013        let root = tree.root_node();
2014        assert!(!root.has_error(), "{}", root.to_sexp());
2015        let string = node_of_kind(root, "string").expect("string literal");
2016        assert_eq!(
2017            PYTHON_STRUCTURAL_SPEC.string_literal_parenthesized(string),
2018            Some(true),
2019            "{}",
2020            root.to_sexp()
2021        );
2022    }
2023
2024    #[test]
2025    #[hardwon("#4797: a line continuation in a dict literal disabled duplicate-key detection")]
2026    fn duplicate_dictionary_key_skips_extras() {
2027        let source = "D = {1: 2, \\\n 1: 3}\n";
2028        let tree = parse_python(source);
2029        let root = tree.root_node();
2030        assert!(!root.has_error(), "{}", root.to_sexp());
2031        let dictionary = node_of_kind(root, "dictionary").expect("dictionary literal");
2032        let mut cursor = dictionary.walk();
2033        let pairs: Vec<Node<'_>> = dictionary
2034            .named_children(&mut cursor)
2035            .filter(|child| child.kind() == "pair")
2036            .collect();
2037        assert_eq!(pairs.len(), 2, "{}", root.to_sexp());
2038        assert_eq!(
2039            PYTHON_STRUCTURAL_SPEC.duplicate_dictionary_key(pairs[1], source),
2040            Some(true),
2041            "{}",
2042            root.to_sexp()
2043        );
2044    }
2045
2046    #[test]
2047    fn django_domain_validator_strings_parse_without_parenthesization_panics() {
2048        let source = r#"
2049class DomainNameValidator(RegexValidator):
2050    message = _("Enter a valid domain name.")
2051    ul = "\u00a1-\uffff"  # Unicode letters range (must not be a raw string).
2052    # Host patterns.
2053    hostname_re = (
2054        r"[a-z" + ul + r"0-9](?:[a-z" + ul + r"0-9-]{0,61}[a-z" + ul + r"0-9])?"
2055    )
2056    # Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1.
2057    domain_re = r"(?:\.(?!-)[a-z" + ul + r"0-9-]{1,63}(?<!-))*"
2058    # Top-level domain.
2059    tld_no_fqdn_re = (
2060        r"\."  # dot
2061        r"(?!-)"  # can't start with a dash
2062        r"(?:[a-z" + ul + "-]{2,63}"  # domain label
2063        r"|xn--[a-z0-9]{1,59})"  # or punycode label
2064        r"(?<!-)"  # can't end with a dash
2065    )
2066    tld_re = tld_no_fqdn_re + r"\.?"
2067    ascii_only_hostname_re = r"[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?"
2068    ascii_only_domain_re = r"(?:\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-))*"
2069    ascii_only_tld_re = (
2070        r"\."  # dot
2071        r"(?!-)"  # can't start with a dash
2072        r"(?:[a-zA-Z0-9-]{2,63})"  # domain label
2073        r"(?<!-)"  # can't end with a dash
2074        r"\.?"  # may have a trailing dot
2075    )
2076"#;
2077        let tree = parse_python(source);
2078        let root = tree.root_node();
2079        assert!(!root.has_error(), "{}", root.to_sexp());
2080        let (string_count, parenthesized_count) = string_literal_parenthesization(root);
2081        assert!(string_count > 0, "{}", root.to_sexp());
2082        assert!(parenthesized_count > 0, "{}", root.to_sexp());
2083    }
2084
2085    #[test]
2086    fn decoded_string_values_match_python_escape_and_concatenation_semantics() {
2087        for (source, expected) in [
2088            (
2089                r#"MIDDLEWARE = "\x64jango.middleware.csrf.CsrfViewMiddleware""#,
2090                Some("django.middleware.csrf.CsrfViewMiddleware"),
2091            ),
2092            (
2093                r#"MIDDLEWARE = "django.middleware.csrf." "CsrfViewMiddleware""#,
2094                Some("django.middleware.csrf.CsrfViewMiddleware"),
2095            ),
2096            (
2097                r#"MIDDLEWARE = b"django.middleware.csrf.CsrfViewMiddleware""#,
2098                None,
2099            ),
2100        ] {
2101            let mut parser = tree_sitter::Parser::new();
2102            parser
2103                .set_language(&tree_sitter_python::LANGUAGE.into())
2104                .expect("Python parser");
2105            let tree = parser.parse(source, None).expect("Python source tree");
2106            let mut pending = vec![tree.root_node()];
2107            let assignment = loop {
2108                let node = pending.pop().expect("Python tree has an assignment");
2109                if node.kind() == "assignment" {
2110                    break node;
2111                }
2112                let mut cursor = node.walk();
2113                pending.extend(node.named_children(&mut cursor));
2114            };
2115            let value = assignment
2116                .child_by_field_name("right")
2117                .expect("assignment value");
2118            assert_eq!(
2119                python_string_key_value(value, source).as_deref(),
2120                expected,
2121                "{}",
2122                tree.root_node().to_sexp()
2123            );
2124        }
2125    }
2126}