1use 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
45pub 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 ("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 ("block", NormalizedKind::Block),
84 ("decorator", NormalizedKind::Decorator),
85];
86
87fn 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
118fn 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
140fn 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 "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
195fn 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 "block" | "decorated_definition" => current = parent,
212 _ => return false,
213 }
214 }
215 false
216}
217
218fn python_binding_activation(binder: Node<'_>, scope: Range) -> Option<BindingActivation> {
229 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304enum ReadableSurface {
305 Yes,
306 No,
307}
308
309fn 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
332fn 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 let Some(value) = expression.child_by_field_name("right") else {
366 continue;
367 };
368 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 "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
405fn 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 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 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 "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 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 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 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 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}