Skip to main content

codesynapse_core/ts_extract/
scala.rs

1use super::{add_contains_edge, add_node_if_missing, make_file_node, run_query_named};
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 TsScalaExtractor;
9
10impl TsScalaExtractor {
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_scala::LANGUAGE.into();
19
20        let class_query = r#"
21            (class_definition
22                name: (identifier) @class.name
23            )
24        "#;
25        if let Ok(mut captures) = run_query_named(source, &lang, class_query) {
26            for name in captures.remove("class.name").unwrap_or_default() {
27                let id = make_id(&[&file_id, &name]);
28                fragment.nodes.push(Node {
29                    id: id.clone(),
30                    label: name,
31                    file_type: "code".to_string(),
32                    source_file: path.to_string_lossy().to_string(),
33                    source_location: None,
34                    community: None,
35                    rationale: None,
36                    docstring: None,
37                    metadata: HashMap::new(),
38                });
39                add_contains_edge(&mut fragment, &file_id, id, path);
40            }
41        }
42
43        let object_query = r#"
44            (object_definition
45                name: (identifier) @object.name
46            )
47        "#;
48        if let Ok(mut captures) = run_query_named(source, &lang, object_query) {
49            for name in captures.remove("object.name").unwrap_or_default() {
50                let id = make_id(&[&file_id, &name]);
51                add_node_if_missing(
52                    &mut fragment,
53                    Node {
54                        id: id.clone(),
55                        label: name,
56                        file_type: "code".to_string(),
57                        source_file: path.to_string_lossy().to_string(),
58                        source_location: None,
59                        community: None,
60                        rationale: None,
61                        docstring: None,
62                        metadata: HashMap::new(),
63                    },
64                );
65                add_contains_edge(&mut fragment, &file_id, id, path);
66            }
67        }
68
69        let method_query = r#"
70            (function_definition
71                name: (identifier) @method.name
72            )
73        "#;
74        if let Ok(mut captures) = run_query_named(source, &lang, method_query) {
75            for name in captures.remove("method.name").unwrap_or_default() {
76                let label = format!("{}()", name);
77                let id = make_id(&[&file_id, &name]);
78                add_node_if_missing(
79                    &mut fragment,
80                    Node {
81                        id: id.clone(),
82                        label,
83                        file_type: "method".to_string(),
84                        source_file: path.to_string_lossy().to_string(),
85                        source_location: None,
86                        community: None,
87                        rationale: None,
88                        docstring: None,
89                        metadata: HashMap::new(),
90                    },
91                );
92                add_contains_edge(&mut fragment, &file_id, id, path);
93            }
94        }
95
96        // tree-sitter-scala 0.26 import_declaration has `path` field with multiple identifiers;
97        // text scan is simpler and reliable for extracting the last path segment
98        let source_str = std::str::from_utf8(source).unwrap_or("");
99        for line in source_str.lines() {
100            let trimmed = line.trim();
101            if !trimmed.starts_with("import ") {
102                continue;
103            }
104            let path_part = trimmed[7..].trim();
105            let name = path_part.split('.').next_back().unwrap_or(path_part).trim();
106            if name.is_empty() || name == "_" || name == "*" || name == "{" {
107                continue;
108            }
109            let name = name.to_string();
110            let mod_id = make_id(&[&file_id, &name]);
111            add_node_if_missing(
112                &mut fragment,
113                Node {
114                    id: mod_id.clone(),
115                    label: name,
116                    file_type: "module".to_string(),
117                    source_file: path.to_string_lossy().to_string(),
118                    source_location: None,
119                    community: None,
120                    rationale: None,
121                    docstring: None,
122                    metadata: HashMap::new(),
123                },
124            );
125            fragment.edges.push(Edge {
126                source: file_id.clone(),
127                target: mod_id,
128                relation: "imports".to_string(),
129                confidence: "EXTRACTED".to_string(),
130                source_file: Some(path.to_string_lossy().to_string()),
131                weight: 1.0,
132                context: Some("import".to_string()),
133            });
134        }
135
136        // Call extraction
137        let call_query = r#"
138            (call_expression
139                (identifier) @call.name
140            )
141        "#;
142        if let Ok(mut captures) = run_query_named(source, &lang, call_query) {
143            for name in captures.remove("call.name").unwrap_or_default() {
144                let callee_id = make_id(&[&file_id, &name]);
145                if fragment.nodes.iter().any(|n| n.id == callee_id) {
146                    add_node_if_missing(
147                        &mut fragment,
148                        Node {
149                            id: callee_id.clone(),
150                            label: format!("{}()", name),
151                            file_type: "function".to_string(),
152                            source_file: path.to_string_lossy().to_string(),
153                            source_location: None,
154                            community: None,
155                            rationale: None,
156                            docstring: None,
157                            metadata: HashMap::new(),
158                        },
159                    );
160                    let already = fragment.edges.iter().any(|e| {
161                        e.relation == "calls" && e.source == file_id && e.target == callee_id
162                    });
163                    if !already {
164                        fragment.edges.push(Edge {
165                            source: file_id.clone(),
166                            target: callee_id,
167                            relation: "calls".to_string(),
168                            confidence: "EXTRACTED".to_string(),
169                            source_file: Some(path.to_string_lossy().to_string()),
170                            weight: 1.0,
171                            context: Some("call".to_string()),
172                        });
173                    }
174                }
175            }
176        }
177
178        Ok(fragment)
179    }
180}
181
182impl LanguageExtractor for TsScalaExtractor {
183    fn file_extensions(&self) -> Vec<&'static str> {
184        vec!["scala"]
185    }
186    fn extract(&self, source: &[u8], path: &Path) -> Result<ExtractionFragment> {
187        Self::extract(source, path)
188    }
189    fn resolve_imports(&self, _imports: &[ImportNode]) -> Vec<Edge> {
190        vec![]
191    }
192    fn collect_type_refs(&self, _fragment: &mut ExtractionFragment) {}
193}