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    attach_argument_role_with_derived_name, attach_role_with_derived_name, attach_terminal_callee,
9    field_name_in_parent, first_named_child, nearest_ancestor, node_range,
10};
11use brokk_bifrost_core::analyzer::structural::callable::CallSiteContext;
12use brokk_bifrost_core::analyzer::structural::edges::{
13    DEEP_REFERENCE_EDGE_SUPPORT, ReferenceEdgeSupport,
14};
15use brokk_bifrost_core::analyzer::structural::kinds::{NormalizedKind, Role};
16use brokk_bifrost_core::analyzer::structural::materialization::{
17    DeclarationMaterializationSupport, PYTHON_MATERIALIZATION_SUPPORT,
18};
19use brokk_bifrost_core::analyzer::structural::occurrences::{
20    Namespace, OccurrenceRole, OccurrenceRoleSupport, default_occurrence_namespace,
21};
22use brokk_bifrost_core::analyzer::structural::resolution::{
23    BindingActivation, BindingKind, DEEP_LEXICAL_ENVIRONMENT_SUPPORT, HoistingClass,
24    LexicalEnvironmentSupport,
25};
26use brokk_bifrost_core::analyzer::structural::routes::{
27    CuratedExportSurface, DEEP_IDENTITY_AXES, IdentityRouteSupport, RouteHopKind,
28};
29use brokk_bifrost_core::analyzer::structural::spec::{EmbeddedLeafFact, RoleSink, StructuralSpec};
30use brokk_bifrost_core::analyzer::{Language, Range};
31use brokk_bifrost_core::cancellation::CancellationToken;
32use brokk_bifrost_core::hash::HashSet;
33use tree_sitter::Node;
34
35use crate::syntax::{
36    expression_name_node, python_deferred_annotation_identifier_ranges,
37    python_keyword_argument_label, python_node_is_in_annotation, python_plain_string_literal,
38};
39
40#[derive(Debug, Default)]
41pub struct PythonStructuralSpec;
42
43pub static PYTHON_STRUCTURAL_SPEC: PythonStructuralSpec = PythonStructuralSpec;
44
45/// Grammar node-type → normalized kind. Every name here must exist in the
46/// tree-sitter-python grammar; `tests::python_kind_table_matches_grammar`
47/// asserts that, so a grammar bump that renames a node fails loudly.
48pub const PYTHON_KIND_TABLE: &[(&str, NormalizedKind)] = &[
49    ("call", NormalizedKind::Call),
50    ("attribute", NormalizedKind::FieldAccess),
51    ("function_definition", NormalizedKind::Function),
52    ("lambda", NormalizedKind::Lambda),
53    ("class_definition", NormalizedKind::Class),
54    ("assignment", NormalizedKind::Assignment),
55    // The implicit `__exit__` of a `with` statement runs at the construct's
56    // own exit, so the statement is the source fact the resource-lifecycle
57    // selector names for that release.
58    ("with_statement", NormalizedKind::ResourceRelease),
59    ("import_statement", NormalizedKind::Import),
60    ("import_from_statement", NormalizedKind::Import),
61    ("identifier", NormalizedKind::Identifier),
62    ("string", NormalizedKind::StringLiteral),
63    ("concatenated_string", NormalizedKind::StringLiteral),
64    ("integer", NormalizedKind::NumericLiteral),
65    ("float", NormalizedKind::NumericLiteral),
66    ("true", NormalizedKind::BooleanLiteral),
67    ("false", NormalizedKind::BooleanLiteral),
68    ("none", NormalizedKind::NullLiteral),
69    ("return_statement", NormalizedKind::Return),
70    ("raise_statement", NormalizedKind::Throw),
71    ("except_clause", NormalizedKind::Catch),
72    ("if_statement", NormalizedKind::If),
73    ("for_statement", NormalizedKind::ForLoop),
74    ("list", NormalizedKind::CollectionLiteral),
75    ("set", NormalizedKind::CollectionLiteral),
76    ("dictionary", NormalizedKind::CollectionLiteral),
77    ("tuple", NormalizedKind::CollectionLiteral),
78    ("while_statement", NormalizedKind::WhileLoop),
79    // Python's indented suite. The module node is deliberately absent: a file
80    // scope is not a statement list nested inside another one, and making the
81    // root a fact in one language only would give Python a scope shape no
82    // other adapter has.
83    ("block", NormalizedKind::Block),
84    ("decorator", NormalizedKind::Decorator),
85];
86
87/// Attach `decorators` edges for a definition wrapped in Python's
88/// `decorated_definition` node (which itself is not normalized).
89fn attach_decorators(sink: &mut RoleSink<'_>, definition: Node<'_>) {
90    let Some(parent) = definition.parent() else {
91        return;
92    };
93    if parent.kind() != "decorated_definition" {
94        return;
95    }
96    for index in 0..parent.named_child_count() {
97        let Some(child) = parent.named_child(index) else {
98            continue;
99        };
100        if child.kind() == "decorator" {
101            attach_role_with_derived_name(sink, Role::Decorator, child, expression_name_node);
102        }
103    }
104}
105
106static PYTHON_OCCURRENCE_ROLE_SUPPORT: OccurrenceRoleSupport = OccurrenceRoleSupport::NONE
107    .supported(OccurrenceRole::DeclarationName)
108    .supported(OccurrenceRole::Binder)
109    .supported(OccurrenceRole::LabelOrKey)
110    .supported(OccurrenceRole::TypeOperand)
111    .supported(OccurrenceRole::PathSegment)
112    .supported(OccurrenceRole::ImportAlias)
113    .supported(OccurrenceRole::ImportTarget)
114    .supported(OccurrenceRole::ReceiverPosition)
115    .supported(OccurrenceRole::MemberPosition)
116    .supported(OccurrenceRole::ValueReference);
117
118/// Whether a `dotted_name` names an imported module rather than an ordinary
119/// attribute chain, which decides whether its tail is an import target.
120///
121/// `relative_import` is the wrapper the grammar puts around the module name of
122/// `from .impl import x`. It is the same import target as the `pkg.impl` of
123/// `from pkg.impl import x`, so it passes through like the other wrappers.
124fn python_dotted_name_is_import(dotted_name: Node<'_>) -> bool {
125    let mut current = dotted_name;
126    loop {
127        let Some(parent) = current.parent() else {
128            return false;
129        };
130        match parent.kind() {
131            "import_statement" | "import_from_statement" | "future_import_statement" => {
132                return true;
133            }
134            "aliased_import" | "dotted_name" | "relative_import" => current = parent,
135            _ => return false,
136        }
137    }
138}
139
140/// Classify one Python identifier token by its AST position.
141///
142/// Python has no separate type-identifier node, so annotation operands are
143/// recognized by their enclosing `type` node — the same field the parser uses
144/// to separate `def f(x: T)`'s binder from its annotation.
145fn python_occurrence_role(node: Node<'_>) -> Option<OccurrenceRole> {
146    if node.kind() != "identifier" {
147        return None;
148    }
149    let parent = node.parent()?;
150    let field = field_name_in_parent(parent, node);
151    let role = match parent.kind() {
152        "function_definition" | "class_definition" if field == Some("name") => {
153            OccurrenceRole::DeclarationName
154        }
155        // Every annotation, return type and type-alias operand is wrapped in a
156        // `type` node; `generic_type`/`type_parameter` nest inside one.
157        "type" | "generic_type" | "type_parameter" | "constrained_type" | "union_type" => {
158            OccurrenceRole::TypeOperand
159        }
160        "parameters"
161        | "lambda_parameters"
162        | "typed_parameter"
163        | "list_splat_pattern"
164        | "dictionary_splat_pattern"
165        | "tuple_pattern"
166        | "list_pattern"
167        | "pattern_list"
168        | "as_pattern_target" => OccurrenceRole::Binder,
169        "default_parameter" | "typed_default_parameter" if field == Some("name") => {
170            OccurrenceRole::Binder
171        }
172        "for_statement" | "for_in_clause" if field == Some("left") => OccurrenceRole::Binder,
173        "keyword_argument" if python_keyword_argument_label(node) => OccurrenceRole::LabelOrKey,
174        "attribute" => match field {
175            Some("attribute") => OccurrenceRole::MemberPosition,
176            Some("object") => OccurrenceRole::ReceiverPosition,
177            _ => OccurrenceRole::ValueReference,
178        },
179        "aliased_import" if field == Some("alias") => OccurrenceRole::ImportAlias,
180        "import_from_statement" if field == Some("name") => OccurrenceRole::ImportTarget,
181        "dotted_name" => {
182            let is_tail =
183                parent.named_child(parent.named_child_count().saturating_sub(1)) == Some(node);
184            match (is_tail, python_dotted_name_is_import(parent)) {
185                (true, true) => OccurrenceRole::ImportTarget,
186                (true, false) => OccurrenceRole::ValueReference,
187                (false, _) => OccurrenceRole::PathSegment,
188            }
189        }
190        _ => OccurrenceRole::ValueReference,
191    };
192    Some(role)
193}
194
195/// Whether a `def` declares a method, that is, whether the suite it sits in
196/// belongs to a `class_definition`.
197///
198/// This reads the parse tree rather than the nearest enclosing normalized kind
199/// because the suite between a class and its methods is itself a normalized
200/// node now (`NormalizedKind::Block`, issue #1474). Walking the concrete
201/// ancestors is also the more direct statement of the rule: a nested `def`
202/// inside a method reaches its enclosing `function_definition` first and stays
203/// a function.
204fn python_definition_is_method(definition: Node<'_>) -> bool {
205    let mut current = definition;
206    while let Some(parent) = current.parent() {
207        match parent.kind() {
208            "class_definition" => return true,
209            // The suite that holds the members, and the wrapper a decorated
210            // definition sits in, are pass-through on the way to the owner.
211            "block" | "decorated_definition" => current = parent,
212            _ => return false,
213        }
214    }
215    false
216}
217
218/// The binding one Python binder token introduces, and the interval it is in
219/// effect over.
220///
221/// Python's function locals are scope-categorical rather than positional: a
222/// name assigned anywhere in a function body is a local of that function for
223/// the whole body, which is why a read above the assignment is an
224/// `UnboundLocalError` rather than a read of an outer name. That is exactly
225/// `ScopeWide`. The one positional exception is a comprehension target, which
226/// lives in the comprehension's own implicit scope; the same exception
227/// `analyzer::python::bindings` records as `PythonComprehensionBinding`.
228fn python_binding_activation(binder: Node<'_>, scope: Range) -> Option<BindingActivation> {
229    // A binder outside every form below is an ordinary assignment target --
230    // the members of a module-level or block-level `a, b = ...` pattern list,
231    // for example -- and Python's scope-categorical rule makes it a local of
232    // its declaring scope for the whole scope. Answering `None` here made the
233    // whole file's lexical environment incomplete as soon as one such binder
234    // existed (scripts/test-cost/greedy.py's `s, k = heapq.heappop(heap)` at
235    // module scope), which failed every code-smells gate that derived it.
236    let Some(form) = nearest_ancestor(binder, |kind| {
237        matches!(
238            kind,
239            "parameters"
240                | "lambda_parameters"
241                | "for_statement"
242                | "for_in_clause"
243                | "as_pattern"
244                | "list_comprehension"
245                | "set_comprehension"
246                | "dictionary_comprehension"
247                | "generator_expression"
248                | "function_definition"
249        )
250    }) else {
251        return Some(BindingActivation {
252            kind: BindingKind::Local,
253            hoisting: HoistingClass::ScopeWide,
254            activation: scope,
255        });
256    };
257    match form.kind() {
258        "parameters" | "lambda_parameters" | "function_definition" => Some(BindingActivation {
259            kind: BindingKind::Parameter,
260            hoisting: HoistingClass::ScopeWide,
261            activation: scope,
262        }),
263        "for_in_clause" => {
264            // A comprehension clause binds only inside the comprehension.
265            let comprehension = nearest_ancestor(form, |kind| {
266                matches!(
267                    kind,
268                    "list_comprehension"
269                        | "set_comprehension"
270                        | "dictionary_comprehension"
271                        | "generator_expression"
272                )
273            })?;
274            Some(BindingActivation {
275                kind: BindingKind::LoopVariable,
276                hoisting: HoistingClass::DeclaredHead,
277                activation: node_range(comprehension),
278            })
279        }
280        "for_statement" => Some(BindingActivation {
281            kind: BindingKind::LoopVariable,
282            hoisting: HoistingClass::ScopeWide,
283            activation: scope,
284        }),
285        "as_pattern" => Some(BindingActivation {
286            kind: BindingKind::PatternBinder,
287            hoisting: HoistingClass::ScopeWide,
288            activation: scope,
289        }),
290        _ => Some(BindingActivation {
291            kind: BindingKind::Local,
292            hoisting: HoistingClass::ScopeWide,
293            activation: scope,
294        }),
295    }
296}
297
298/// The text one plain string literal denotes, or `None` when the literal is
299/// not plain: an f-string interpolation, an escape sequence this reader does
300/// not decode, or an implicit concatenation. `None` is never an empty name --
301/// it is "this value is computed", which makes the whole surface unreadable.
302/// Whether every member of a curated surface was read from the parse tree.
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304enum ReadableSurface {
305    Yes,
306    No,
307}
308
309/// Collect the members of an `__all__` value into `names`, reporting whether
310/// every member was readable. Only a list or tuple display of plain string
311/// literals is; anything else is a value the source computes.
312fn python_collect_all_members(
313    value: Node<'_>,
314    source: &str,
315    names: &mut HashSet<String>,
316) -> ReadableSurface {
317    if !matches!(value.kind(), "list" | "tuple") {
318        return ReadableSurface::No;
319    }
320    let mut cursor = value.walk();
321    for element in value.named_children(&mut cursor) {
322        match python_plain_string_literal(element, source) {
323            Some(text) => {
324                names.insert(text.to_owned());
325            }
326            None => return ReadableSurface::No,
327        }
328    }
329    ReadableSurface::Yes
330}
331
332/// The names a Python module curates as its public surface: the value of its
333/// module-level `__all__`.
334///
335/// Only the module's own statements are read. An `__all__` inside a function
336/// is a local rather than the module's surface, and a name the module binds
337/// conditionally is still bound by the statement this reader already sees.
338/// A statement that assigns, extends, or mutates `__all__` with anything but a
339/// list or tuple of plain string literals makes the surface unreadable: the
340/// members are then unknown, and no import is classified from them.
341fn python_curated_export_surface(root: Node<'_>, source: &str) -> CuratedExportSurface {
342    let names_all = |node: Option<Node<'_>>| {
343        node.is_some_and(|node| {
344            node.kind() == "identifier" && node_source_text(node, source) == "__all__"
345        })
346    };
347    let mut names: HashSet<String> = HashSet::default();
348    let mut stated = false;
349    let mut readable = ReadableSurface::Yes;
350    let mut cursor = root.walk();
351    for statement in root.named_children(&mut cursor) {
352        if statement.kind() != "expression_statement" {
353            continue;
354        }
355        let mut inner = statement.walk();
356        for expression in statement.named_children(&mut inner) {
357            match expression.kind() {
358                "assignment" | "augmented_assignment" => {
359                    if !names_all(expression.child_by_field_name("left")) {
360                        continue;
361                    }
362                    stated = true;
363                    // An annotation without a value (`__all__: list[str]`)
364                    // binds nothing, so it states no members either way.
365                    let Some(value) = expression.child_by_field_name("right") else {
366                        continue;
367                    };
368                    // `+=` extends the surface; every other augmentation is a
369                    // value this reader does not compute.
370                    let extends = expression.kind() == "assignment"
371                        || expression
372                            .child_by_field_name("operator")
373                            .is_some_and(|operator| operator.kind() == "+=");
374                    if !extends
375                        || python_collect_all_members(value, source, &mut names)
376                            == ReadableSurface::No
377                    {
378                        readable = ReadableSurface::No;
379                    }
380                }
381                // `__all__.extend(other)` and its siblings rewrite the surface
382                // from a value the reader cannot see.
383                "call" => {
384                    let Some(function) = expression.child_by_field_name("function") else {
385                        continue;
386                    };
387                    if function.kind() == "attribute"
388                        && names_all(function.child_by_field_name("object"))
389                    {
390                        stated = true;
391                        readable = ReadableSurface::No;
392                    }
393                }
394                _ => {}
395            }
396        }
397    }
398    match (stated, readable) {
399        (false, _) => CuratedExportSurface::Absent,
400        (true, ReadableSurface::Yes) => CuratedExportSurface::Listed(names),
401        (true, ReadableSurface::No) => CuratedExportSurface::Unreadable,
402    }
403}
404
405/// Which indirection relation one Python import token participates in.
406///
407/// Python's grammar names no re-export, so the relation follows the explicit
408/// re-export rules the typing ecosystem already enforces (PEP 484 stub
409/// semantics, applied by pyright and mypy in strict mode), which makes this
410/// relation agree with what a type checker calls public:
411///
412/// 1. A name on the module's `__all__` is a re-export of whatever binding the
413///    module gives that name.
414/// 2. The redundant-alias forms `from x import y as y` and `import x as x`
415///    are re-exports.
416/// 3. `from x import *` is one star hop that forwards the public surface of
417///    `x`; the expansion is the import machinery's work, not this producer's,
418///    so the hop is recorded on the module reference and nothing is
419///    enumerated here.
420/// 4. Every other import is an ordinary import, including a plain
421///    `from .impl import helper` in a package `__init__.py` that states no
422///    `__all__`. The facade convention alone does not make a name public, and
423///    a consumer that wants every name a facade imports already has the
424///    import relation.
425///
426/// `None` is the answer for a name whose membership only an unreadable
427/// `__all__` could settle; the file's relations then report incomplete rather
428/// than guessing either way.
429fn python_indirection_relation(
430    token: Node<'_>,
431    source: &str,
432    surface: &CuratedExportSurface,
433) -> Option<RouteHopKind> {
434    let statement = nearest_ancestor(token, |kind| {
435        matches!(
436            kind,
437            "import_statement" | "import_from_statement" | "future_import_statement"
438        )
439    })?;
440    // The statement's own child that holds this token: a `module_name` field,
441    // or one `name` field of the import list.
442    let mut clause = token;
443    while let Some(parent) = clause.parent() {
444        if parent.id() == statement.id() {
445            break;
446        }
447        clause = parent;
448    }
449
450    if field_name_in_parent(statement, clause) == Some("module_name") {
451        // `from x import a` binds `a`, not `x`, so the module reference
452        // forwards nothing -- unless the import is the star form, whose one
453        // hop forwards the whole surface of `x`.
454        let mut cursor = statement.walk();
455        let star = statement
456            .children(&mut cursor)
457            .any(|child| child.kind() == "wildcard_import");
458        return Some(if star {
459            RouteHopKind::ReExport
460        } else {
461            RouteHopKind::Import
462        });
463    }
464
465    let bound = match clause.kind() {
466        "aliased_import" => {
467            let name = clause.child_by_field_name("name")?;
468            let alias = clause.child_by_field_name("alias")?;
469            if node_source_text(name, source) == node_source_text(alias, source) {
470                return Some(RouteHopKind::ReExport);
471            }
472            alias
473        }
474        // `import a.b.c` binds the top package `a`; `from m import a` binds
475        // the single-segment name the import list spells.
476        "dotted_name" if statement.kind() == "import_statement" => clause.named_child(0)?,
477        "dotted_name" => clause,
478        _ => return None,
479    };
480    match surface.lists(node_source_text(bound, source)) {
481        Some(true) => Some(RouteHopKind::ReExport),
482        Some(false) => Some(RouteHopKind::Import),
483        None => None,
484    }
485}
486
487impl StructuralSpec for PythonStructuralSpec {
488    fn language(&self) -> Language {
489        Language::Python
490    }
491
492    fn supports_boolean_literal_value(&self) -> bool {
493        true
494    }
495
496    fn reference_edge_support(&self) -> &ReferenceEdgeSupport {
497        &DEEP_REFERENCE_EDGE_SUPPORT
498    }
499
500    fn identity_route_support(&self) -> &IdentityRouteSupport {
501        // `import x as y` is an alias, and `python_indirection_relation`
502        // states which imports re-export (issue #1649).
503        static SUPPORT: IdentityRouteSupport = DEEP_IDENTITY_AXES
504            .supported_relation(RouteHopKind::Alias)
505            .supported_relation(RouteHopKind::Import)
506            .supported_relation(RouteHopKind::ReExport)
507            .supported_relation(RouteHopKind::NestedOwner);
508        &SUPPORT
509    }
510
511    /// Python's one qualified-path chain is `dotted_name`, which is flat
512    /// rather than left-nested: its named children are the segments in order.
513    fn qualified_path_root<'tree>(&self, token: Node<'tree>) -> Option<Node<'tree>> {
514        if token.kind() != "identifier" {
515            return None;
516        }
517        token
518            .parent()
519            .filter(|parent| parent.kind() == "dotted_name")
520    }
521
522    fn path_segment_tokens<'tree>(&self, root: Node<'tree>) -> Vec<Node<'tree>> {
523        if root.kind() != "dotted_name" {
524            return Vec::new();
525        }
526        let mut cursor = root.walk();
527        root.named_children(&mut cursor)
528            .filter(|child| child.kind() == "identifier")
529            .collect()
530    }
531
532    fn curated_export_surface(&self, root: Node<'_>, source: &str) -> CuratedExportSurface {
533        python_curated_export_surface(root, source)
534    }
535
536    fn indirection_relation(
537        &self,
538        token: Node<'_>,
539        source: &str,
540        surface: &CuratedExportSurface,
541    ) -> Option<RouteHopKind> {
542        python_indirection_relation(token, source, surface)
543    }
544
545    fn kind_table(&self) -> &'static [(&'static str, NormalizedKind)] {
546        PYTHON_KIND_TABLE
547    }
548
549    fn refine_kind(
550        &self,
551        node: Node<'_>,
552        kind: NormalizedKind,
553        _enclosing: Option<NormalizedKind>,
554        _source: &str,
555        _context: &CallSiteContext,
556    ) -> NormalizedKind {
557        if kind == NormalizedKind::Function && python_definition_is_method(node) {
558            NormalizedKind::Method
559        } else {
560            kind
561        }
562    }
563
564    fn should_extract(&self, node: Node<'_>, kind: NormalizedKind) -> bool {
565        kind != NormalizedKind::Assignment || node.child_by_field_name("right").is_some()
566    }
567
568    fn supports_kind(&self, kind: NormalizedKind) -> bool {
569        kind == NormalizedKind::Method
570            || self
571                .kind_table()
572                .iter()
573                .any(|(_, fact_kind)| fact_kind.satisfies(kind))
574    }
575
576    fn occurrence_role_support(&self) -> &OccurrenceRoleSupport {
577        &PYTHON_OCCURRENCE_ROLE_SUPPORT
578    }
579
580    fn lexical_environment_support(&self) -> &LexicalEnvironmentSupport {
581        &DEEP_LEXICAL_ENVIRONMENT_SUPPORT
582    }
583
584    fn materialization_support(&self) -> &DeclarationMaterializationSupport {
585        &PYTHON_MATERIALIZATION_SUPPORT
586    }
587
588    fn binding_activation(&self, binder: Node<'_>, scope: Range) -> Option<BindingActivation> {
589        python_binding_activation(binder, scope)
590    }
591
592    /// Python only classifies a scope segment inside a `dotted_name`, and every
593    /// non-tail segment of a dotted name is a module.
594    fn occurrence_namespace(
595        &self,
596        role: OccurrenceRole,
597        declares: Option<NormalizedKind>,
598    ) -> Option<Namespace> {
599        match role {
600            OccurrenceRole::PathSegment => Some(Namespace::Module),
601            _ => default_occurrence_namespace(role, declares),
602        }
603    }
604
605    fn embedded_leaf_facts(
606        &self,
607        node: Node<'_>,
608        kind: NormalizedKind,
609        source: &str,
610        cancellation: Option<&CancellationToken>,
611    ) -> Vec<EmbeddedLeafFact> {
612        if kind != NormalizedKind::StringLiteral
613            || node.kind() != "string"
614            || !python_node_is_in_annotation(node)
615        {
616            return Vec::new();
617        }
618
619        python_deferred_annotation_identifier_ranges(node, source, cancellation)
620            .unwrap_or_default()
621            .into_iter()
622            .map(|range| EmbeddedLeafFact {
623                kind: NormalizedKind::Identifier,
624                range,
625                occurrence_role: OccurrenceRole::TypeOperand,
626            })
627            .collect()
628    }
629
630    fn extract(&self, node: Node<'_>, kind: NormalizedKind, sink: &mut RoleSink<'_>) {
631        if let Some(role) = python_occurrence_role(node) {
632            sink.occurrence_role(node, role);
633        }
634        match kind {
635            NormalizedKind::Call => {
636                if let Some(function) = node.child_by_field_name("function") {
637                    // A call's own name is its callee's, so
638                    // { "kind": "call", "name": "eval" } reads naturally.
639                    attach_terminal_callee(sink, function, expression_name_node(function));
640                    if function.kind() == "attribute"
641                        && let Some(object) = function.child_by_field_name("object")
642                    {
643                        attach_role_with_derived_name(
644                            sink,
645                            Role::Receiver,
646                            object,
647                            expression_name_node,
648                        );
649                    }
650                }
651                if let Some(arguments) = node.child_by_field_name("arguments") {
652                    for index in 0..arguments.named_child_count() {
653                        if !sink.should_continue() {
654                            break;
655                        }
656                        let Some(argument) = arguments.named_child(index) else {
657                            continue;
658                        };
659                        match argument.kind() {
660                            "comment" => {}
661                            "keyword_argument" => {
662                                if let (Some(keyword), Some(value)) = (
663                                    argument.child_by_field_name("name"),
664                                    argument.child_by_field_name("value"),
665                                ) {
666                                    sink.kwarg(keyword, value);
667                                }
668                            }
669                            _ => attach_argument_role_with_derived_name(
670                                sink,
671                                argument,
672                                expression_name_node,
673                            ),
674                        }
675                    }
676                }
677            }
678            NormalizedKind::FieldAccess => {
679                if let Some(attribute) = node.child_by_field_name("attribute") {
680                    sink.set_name(attribute);
681                    sink.role_named(Role::Field, attribute, attribute);
682                }
683                if let Some(object) = node.child_by_field_name("object") {
684                    attach_role_with_derived_name(sink, Role::Object, object, expression_name_node);
685                }
686            }
687            NormalizedKind::Function | NormalizedKind::Method | NormalizedKind::Class => {
688                if let Some(name) = node.child_by_field_name("name") {
689                    sink.set_name(name);
690                }
691                attach_decorators(sink, node);
692            }
693            NormalizedKind::Assignment => {
694                if let Some(left) = node.child_by_field_name("left") {
695                    attach_role_with_derived_name(sink, Role::Left, left, expression_name_node);
696                }
697                if let Some(right) = node.child_by_field_name("right") {
698                    attach_role_with_derived_name(sink, Role::Right, right, expression_name_node);
699                }
700            }
701            NormalizedKind::Import => match node.kind() {
702                "import_from_statement" => {
703                    if let Some(module) = node.child_by_field_name("module_name") {
704                        sink.role_named(Role::Module, module, module);
705                    }
706                }
707                _ => {
708                    for index in 0..node.named_child_count() {
709                        if !sink.should_continue() {
710                            break;
711                        }
712                        let Some(child) = node.named_child(index) else {
713                            continue;
714                        };
715                        match child.kind() {
716                            "dotted_name" => sink.role_named(Role::Module, child, child),
717                            "aliased_import" => {
718                                if let Some(name) = child.child_by_field_name("name") {
719                                    sink.role_named(Role::Module, name, name);
720                                }
721                            }
722                            _ => {}
723                        }
724                    }
725                }
726            },
727            NormalizedKind::Identifier => sink.set_name(node),
728            NormalizedKind::Decorator => {
729                if let Some(name) = first_named_child(node).and_then(expression_name_node) {
730                    sink.set_name(name);
731                }
732            }
733            NormalizedKind::ForLoop => {
734                if let Some(right) = node.child_by_field_name("right") {
735                    attach_role_with_derived_name(
736                        sink,
737                        Role::Iterable,
738                        right,
739                        expression_name_node,
740                    );
741                }
742            }
743            NormalizedKind::CollectionLiteral => {
744                for index in 0..node.named_child_count() {
745                    let Some(child) = node.named_child(index) else {
746                        continue;
747                    };
748                    if child.kind() == "comment" {
749                        continue;
750                    }
751                    attach_role_with_derived_name(sink, Role::Element, child, expression_name_node);
752                }
753            }
754            _ => {}
755        }
756    }
757}