Skip to main content

brokk_bifrost_ruby/graph/
syntax.rs

1use 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}