Skip to main content

codesynapse_core/ts_extract/
ruby.rs

1use super::{add_contains_edge, add_node_if_missing, make_file_node, run_query_matches_ranged};
2use crate::error::Result;
3use crate::extract::{make_id, ImportNode, LanguageExtractor};
4use crate::types::{Edge, ExtractionFragment, Node};
5use std::collections::HashMap;
6use std::path::Path;
7
8pub struct TsRubyExtractor;
9
10impl TsRubyExtractor {
11    pub fn extract(source: &[u8], path: &Path) -> Result<ExtractionFragment> {
12        let (file_id, _, file_node) = make_file_node(path);
13        let mut fragment = ExtractionFragment {
14            nodes: vec![file_node],
15            edges: vec![],
16        };
17
18        let lang = tree_sitter_ruby::LANGUAGE.into();
19
20        // Classes with source_location and superclass inherits edges
21        let class_query = r#"
22            (class
23                name: (constant) @class.name
24                superclass: (superclass (constant) @class.super)?
25            ) @class.node
26        "#;
27        if let Ok(matches) = run_query_matches_ranged(source, &lang, class_query) {
28            for (texts, ranges) in &matches {
29                let name = match texts.get("class.name") {
30                    Some(n) => n.clone(),
31                    None => continue,
32                };
33                let source_location = ranges
34                    .get("class.node")
35                    .map(|(s, e)| format!("{}:{}", s, e));
36                let class_id = make_id(&[&file_id, &name]);
37                fragment.nodes.push(Node {
38                    id: class_id.clone(),
39                    label: name.clone(),
40                    file_type: "class".to_string(),
41                    source_file: path.to_string_lossy().to_string(),
42                    source_location,
43                    community: None,
44                    rationale: None,
45                    docstring: None,
46                    metadata: {
47                        let mut m = HashMap::new();
48                        m.insert("kind".to_string(), "class".to_string());
49                        m
50                    },
51                });
52                add_contains_edge(&mut fragment, &file_id, class_id.clone(), path);
53
54                if let Some(super_name) = texts.get("class.super") {
55                    let super_id = make_id(&[super_name]);
56                    add_node_if_missing(
57                        &mut fragment,
58                        Node {
59                            id: super_id.clone(),
60                            label: super_name.clone(),
61                            file_type: "code".to_string(),
62                            source_file: path.to_string_lossy().to_string(),
63                            source_location: None,
64                            community: None,
65                            rationale: None,
66                            docstring: None,
67                            metadata: HashMap::new(),
68                        },
69                    );
70                    fragment.edges.push(Edge {
71                        source: class_id,
72                        target: super_id,
73                        relation: "inherits".to_string(),
74                        confidence: "EXTRACTED".to_string(),
75                        source_file: Some(path.to_string_lossy().to_string()),
76                        weight: 1.0,
77                        context: None,
78                    });
79                }
80            }
81        }
82
83        // Instance methods
84        let method_query = r#"
85            (method name: (identifier) @fn.name) @fn.node
86        "#;
87        if let Ok(matches) = run_query_matches_ranged(source, &lang, method_query) {
88            for (texts, ranges) in &matches {
89                let name = match texts.get("fn.name") {
90                    Some(n) => n.clone(),
91                    None => continue,
92                };
93                let source_location = ranges.get("fn.node").map(|(s, e)| format!("{}:{}", s, e));
94                let method_id = make_id(&[&file_id, &name]);
95                let label = format!("{}()", name);
96                add_node_if_missing(
97                    &mut fragment,
98                    Node {
99                        id: method_id.clone(),
100                        label,
101                        file_type: "method".to_string(),
102                        source_file: path.to_string_lossy().to_string(),
103                        source_location,
104                        community: None,
105                        rationale: None,
106                        docstring: None,
107                        metadata: {
108                            let mut m = HashMap::new();
109                            m.insert("kind".to_string(), "method".to_string());
110                            m
111                        },
112                    },
113                );
114                add_contains_edge(&mut fragment, &file_id, method_id, path);
115            }
116        }
117
118        // Class (singleton) methods
119        let singleton_query = r#"
120            (singleton_method name: (identifier) @fn.name) @fn.node
121        "#;
122        if let Ok(matches) = run_query_matches_ranged(source, &lang, singleton_query) {
123            for (texts, ranges) in &matches {
124                let name = match texts.get("fn.name") {
125                    Some(n) => n.clone(),
126                    None => continue,
127                };
128                let source_location = ranges.get("fn.node").map(|(s, e)| format!("{}:{}", s, e));
129                let method_id = make_id(&[&file_id, "self", &name]);
130                let label = format!("self.{}()", name);
131                add_node_if_missing(
132                    &mut fragment,
133                    Node {
134                        id: method_id.clone(),
135                        label,
136                        file_type: "method".to_string(),
137                        source_file: path.to_string_lossy().to_string(),
138                        source_location,
139                        community: None,
140                        rationale: None,
141                        docstring: None,
142                        metadata: {
143                            let mut m = HashMap::new();
144                            m.insert("kind".to_string(), "method".to_string());
145                            m
146                        },
147                    },
148                );
149                add_contains_edge(&mut fragment, &file_id, method_id, path);
150            }
151        }
152
153        // Class → instance method contains edges
154        let class_method_query = r#"
155            (class
156                name: (constant) @class.name
157                body: (body_statement
158                    (method name: (identifier) @method.name)
159                )
160            )
161        "#;
162        if let Ok(matches) = run_query_matches_ranged(source, &lang, class_method_query) {
163            for (texts, _) in &matches {
164                let class_name = match texts.get("class.name") {
165                    Some(n) => n.clone(),
166                    None => continue,
167                };
168                let method_name = match texts.get("method.name") {
169                    Some(n) => n.clone(),
170                    None => continue,
171                };
172                let class_id = make_id(&[&file_id, &class_name]);
173                let method_id = make_id(&[&file_id, &method_name]);
174                fragment.edges.push(Edge {
175                    source: class_id,
176                    target: method_id,
177                    relation: "contains".to_string(),
178                    confidence: "EXTRACTED".to_string(),
179                    source_file: Some(path.to_string_lossy().to_string()),
180                    weight: 1.0,
181                    context: None,
182                });
183            }
184        }
185
186        // Class → singleton method contains edges
187        let class_singleton_query = r#"
188            (class
189                name: (constant) @class.name
190                body: (body_statement
191                    (singleton_method name: (identifier) @method.name)
192                )
193            )
194        "#;
195        if let Ok(matches) = run_query_matches_ranged(source, &lang, class_singleton_query) {
196            for (texts, _) in &matches {
197                let class_name = match texts.get("class.name") {
198                    Some(n) => n.clone(),
199                    None => continue,
200                };
201                let method_name = match texts.get("method.name") {
202                    Some(n) => n.clone(),
203                    None => continue,
204                };
205                let class_id = make_id(&[&file_id, &class_name]);
206                let method_id = make_id(&[&file_id, "self", &method_name]);
207                fragment.edges.push(Edge {
208                    source: class_id,
209                    target: method_id,
210                    relation: "contains".to_string(),
211                    confidence: "EXTRACTED".to_string(),
212                    source_file: Some(path.to_string_lossy().to_string()),
213                    weight: 1.0,
214                    context: None,
215                });
216            }
217        }
218
219        Ok(fragment)
220    }
221}
222
223impl LanguageExtractor for TsRubyExtractor {
224    fn file_extensions(&self) -> Vec<&'static str> {
225        vec!["rb"]
226    }
227    fn extract(&self, source: &[u8], path: &Path) -> Result<ExtractionFragment> {
228        Self::extract(source, path)
229    }
230    fn resolve_imports(&self, _imports: &[ImportNode]) -> Vec<Edge> {
231        vec![]
232    }
233    fn collect_type_refs(&self, _fragment: &mut ExtractionFragment) {}
234}