brokk_bifrost_ruby/graph/
syntax.rs1use crate::graph::resolver::ReceiverMode;
2use tree_sitter::Node;
3
4pub fn is_declaration_constant(node: Node<'_>) -> bool {
5 if let Some(parent) = node.parent()
6 && matches!(parent.kind(), "class" | "module")
7 && parent.child_by_field_name("name") == Some(node)
8 {
9 return true;
10 }
11 if is_assignment_left_constant(node) {
12 return true;
13 }
14 false
15}
16
17fn is_assignment_left_constant(node: Node<'_>) -> bool {
18 let mut topmost = node;
19 while let Some(parent) = topmost.parent() {
20 if parent.kind() == "assignment" {
21 if parent.child_by_field_name("left") != Some(topmost) {
22 return false;
23 }
24 return node == topmost
25 || topmost
26 .child_by_field_name("name")
27 .is_some_and(|name| name == node);
28 }
29 if parent.kind() != "scope_resolution" {
30 return false;
31 }
32 topmost = parent;
33 }
34 false
35}
36
37pub fn method_receiver_mode(node: Node<'_>) -> ReceiverMode {
38 if node.kind() == "singleton_method" {
39 return ReceiverMode::Class;
40 }
41 let mut parent = node.parent();
42 while let Some(current) = parent {
43 if current.kind() == "singleton_class" {
44 return ReceiverMode::Class;
45 }
46 if matches!(current.kind(), "class" | "module") {
47 break;
48 }
49 parent = current.parent();
50 }
51 if has_enclosing_type(node) {
52 ReceiverMode::Instance
53 } else {
54 ReceiverMode::TopLevel
55 }
56}
57
58fn has_enclosing_type(node: Node<'_>) -> bool {
59 let mut parent = node.parent();
60 while let Some(current) = parent {
61 if matches!(current.kind(), "class" | "module") {
62 return true;
63 }
64 parent = current.parent();
65 }
66 false
67}
68
69pub fn is_declaration_identifier(node: Node<'_>) -> bool {
70 if let Some(parent) = node.parent()
71 && matches!(parent.kind(), "method" | "singleton_method" | "assignment")
72 && parent.child_by_field_name("name") == Some(node)
73 {
74 return true;
75 }
76 if let Some(parent) = node.parent()
77 && parent.kind() == "assignment"
78 && parent.child_by_field_name("left") == Some(node)
79 {
80 return true;
81 }
82 false
83}
84
85pub fn is_plain_assignment_left_variable(node: Node<'_>) -> bool {
86 if !matches!(node.kind(), "instance_variable" | "class_variable") {
87 return false;
88 }
89 node.parent().is_some_and(|parent| {
90 matches!(parent.kind(), "assignment" | "operator_assignment")
91 && parent.child_by_field_name("left") == Some(node)
92 })
93}
94
95pub fn is_call_method_identifier(node: Node<'_>) -> bool {
96 node.parent().is_some_and(|parent| {
97 parent.kind() == "call" && parent.child_by_field_name("method") == Some(node)
98 })
99}
100
101pub fn dynamic_dispatch_target_argument<'tree>(
102 node: Node<'tree>,
103 source: &str,
104) -> Option<(String, Node<'tree>)> {
105 let method = node.child_by_field_name("method")?;
106 if !is_dynamic_dispatch_method(method, source) {
107 return None;
108 }
109 let arguments = node.child_by_field_name("arguments")?;
110 let mut cursor = arguments.walk();
111 let first_argument = arguments.named_children(&mut cursor).next()?;
112 symbol_or_string_value(first_argument, source).map(|member| (member, first_argument))
113}
114
115pub fn is_dynamic_dispatch_method(method: Node<'_>, source: &str) -> bool {
116 matches!(
117 node_text(method, source),
118 "send" | "__send__" | "public_send"
119 )
120}
121
122pub fn constant_hit_node(node: Node<'_>) -> Node<'_> {
123 if node.kind() == "scope_resolution" {
124 node.child_by_field_name("name").unwrap_or(node)
125 } else {
126 node
127 }
128}
129
130pub fn symbol_or_string_value(node: Node<'_>, source: &str) -> Option<String> {
131 let text = node_text(node, source);
132 let stripped = text
133 .strip_prefix(':')
134 .unwrap_or(text)
135 .trim_matches(['"', '\'']);
136 (!stripped.is_empty()).then(|| stripped.to_string())
137}
138
139pub fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
140 brokk_bifrost_core::analyzer::common::node_source_text_trimmed(node, source)
141}