1use super::{add_node_if_missing, make_file_node};
2use crate::error::{CodeSynapseError, Result};
3use crate::extract::{make_id, ImportNode, LanguageExtractor};
4use crate::types::{Edge, ExtractionFragment, Node};
5use std::collections::{HashMap, HashSet};
6use std::path::Path;
7use tree_sitter::{Node as TsNode, Parser};
8
9pub struct TsSwiftExtractor;
10
11fn sw_text(source: &[u8], node: &TsNode<'_>) -> String {
12 std::str::from_utf8(&source[node.start_byte()..node.end_byte()])
13 .unwrap_or("")
14 .trim()
15 .to_string()
16}
17
18fn sw_node(id: String, label: String, file_type: &str, path: &Path) -> Node {
19 Node {
20 id,
21 label,
22 file_type: file_type.to_string(),
23 source_file: path.to_string_lossy().to_string(),
24 source_location: None,
25 community: None,
26 rationale: None,
27 docstring: None,
28 metadata: HashMap::new(),
29 }
30}
31
32fn prescan_protocols(root: TsNode<'_>, source: &[u8]) -> HashSet<String> {
33 let mut protos = HashSet::new();
34 fn walk(node: TsNode<'_>, source: &[u8], protos: &mut HashSet<String>) {
35 if node.kind() == "protocol_declaration" {
36 for i in 0..node.child_count() {
37 if let Some(child) = node.child(i) {
38 if child.kind() == "type_identifier" {
39 let name = sw_text(source, &child);
40 if !name.is_empty() {
41 protos.insert(name);
42 }
43 break;
44 }
45 }
46 }
47 }
48 for i in 0..node.child_count() {
49 if let Some(child) = node.child(i) {
50 walk(child, source, protos);
51 }
52 }
53 }
54 walk(root, source, &mut protos);
55 protos
56}
57
58impl TsSwiftExtractor {
59 pub fn extract(source: &[u8], path: &Path) -> Result<ExtractionFragment> {
60 let (file_id, _, file_node) = make_file_node(path);
61 let mut fragment = ExtractionFragment {
62 nodes: vec![file_node],
63 edges: vec![],
64 };
65
66 let lang = tree_sitter_swift::LANGUAGE.into();
67 let mut parser = Parser::new();
68 parser
69 .set_language(&lang)
70 .map_err(|e| CodeSynapseError::Parse(format!("swift lang: {}", e)))?;
71
72 let tree = parser
73 .parse(source, None)
74 .ok_or_else(|| CodeSynapseError::Parse("swift parse failed".to_string()))?;
75
76 let stem = file_id.clone();
77
78 let protocols = prescan_protocols(tree.root_node(), source);
79
80 Self::walk(
81 tree.root_node(),
82 source,
83 path,
84 &file_id,
85 &stem,
86 &protocols,
87 &mut fragment,
88 );
89
90 Ok(fragment)
91 }
92
93 fn walk(
94 node: TsNode<'_>,
95 source: &[u8],
96 path: &Path,
97 file_id: &str,
98 stem: &str,
99 protocols: &HashSet<String>,
100 fragment: &mut ExtractionFragment,
101 ) {
102 match node.kind() {
103 "import_declaration" => {
104 for i in 0..node.child_count() {
106 if let Some(child) = node.child(i) {
107 if child.kind() == "identifier" {
108 let name = sw_text(source, &child);
109 if !name.is_empty() {
110 let id = make_id(&[&name]);
111 add_node_if_missing(
112 fragment,
113 sw_node(id.clone(), name, "module", path),
114 );
115 fragment.edges.push(Edge {
116 source: file_id.to_string(),
117 target: id,
118 relation: "imports".to_string(),
119 confidence: "EXTRACTED".to_string(),
120 source_file: Some(path.to_string_lossy().to_string()),
121 weight: 1.0,
122 context: Some("import".to_string()),
123 });
124 break;
125 }
126 }
127 }
128 }
129 }
130 "class_declaration" => {
131 let type_name = node
133 .child_by_field_name("name")
134 .map(|n| sw_text(source, &n))
135 .filter(|t| !t.is_empty());
136 if let Some(type_name) = type_name {
137 let type_id = make_id(&[stem, &type_name]);
138 add_node_if_missing(
139 fragment,
140 sw_node(type_id.clone(), type_name.clone(), "class", path),
141 );
142 fragment.edges.push(Edge {
143 source: file_id.to_string(),
144 target: type_id.clone(),
145 relation: "contains".to_string(),
146 confidence: "EXTRACTED".to_string(),
147 source_file: Some(path.to_string_lossy().to_string()),
148 weight: 1.0,
149 context: None,
150 });
151
152 for i in 0..node.child_count() {
154 if let Some(child) = node.child(i) {
155 if child.kind() == "inheritance_specifier" {
156 Self::handle_inheritance_specifier(
157 &child, source, path, &type_id, fragment, protocols, stem,
158 );
159 }
160 }
161 }
162
163 if let Some(body) = node.child_by_field_name("body") {
165 Self::walk_body(
166 body, source, path, file_id, stem, &type_id, protocols, fragment,
167 );
168 }
169 }
170 }
171 "protocol_declaration" => {
172 let type_name = {
173 let mut found = None;
174 for i in 0..node.child_count() {
175 if let Some(child) = node.child(i) {
176 if child.kind() == "type_identifier" {
177 let t = sw_text(source, &child);
178 if !t.is_empty() {
179 found = Some(t);
180 break;
181 }
182 }
183 }
184 }
185 found
186 };
187 if let Some(type_name) = type_name {
188 let type_id = make_id(&[stem, &type_name]);
189 add_node_if_missing(
190 fragment,
191 sw_node(type_id.clone(), type_name, "trait", path),
192 );
193 fragment.edges.push(Edge {
194 source: file_id.to_string(),
195 target: type_id,
196 relation: "contains".to_string(),
197 confidence: "EXTRACTED".to_string(),
198 source_file: Some(path.to_string_lossy().to_string()),
199 weight: 1.0,
200 context: None,
201 });
202 }
203 }
204 "function_declaration" => {
205 Self::extract_function(
207 node, source, path, file_id, stem, file_id, protocols, fragment,
208 );
209 }
210 _ => {
211 for i in 0..node.child_count() {
212 if let Some(child) = node.child(i) {
213 Self::walk(child, source, path, file_id, stem, protocols, fragment);
214 }
215 }
216 }
217 }
218 }
219
220 fn handle_inheritance_specifier(
221 spec: &TsNode<'_>,
222 source: &[u8],
223 path: &Path,
224 type_id: &str,
225 fragment: &mut ExtractionFragment,
226 protocols: &HashSet<String>,
227 stem: &str,
228 ) {
229 let user_type = spec.child_by_field_name("inherits_from");
231 if let Some(ut) = user_type {
232 if ut.kind() == "user_type" {
233 for i in 0..ut.child_count() {
234 if let Some(child) = ut.child(i) {
235 if child.kind() == "type_identifier" {
236 let base_name = sw_text(source, &child);
237 if !base_name.is_empty() {
238 let base_id = make_id(&[stem, &base_name]);
239 add_node_if_missing(
240 fragment,
241 sw_node(base_id.clone(), base_name.clone(), "code", path),
242 );
243 let relation = if protocols.contains(&base_name) {
244 "implements"
245 } else {
246 "inherits"
247 };
248 let already = fragment.edges.iter().any(|e| {
249 e.relation == relation
250 && e.source == type_id
251 && e.target == base_id
252 });
253 if !already {
254 fragment.edges.push(Edge {
255 source: type_id.to_string(),
256 target: base_id,
257 relation: relation.to_string(),
258 confidence: "EXTRACTED".to_string(),
259 source_file: Some(path.to_string_lossy().to_string()),
260 weight: 1.0,
261 context: None,
262 });
263 }
264 }
265 break;
266 }
267 }
268 }
269 }
270 }
271 }
272
273 #[allow(clippy::too_many_arguments)]
274 fn walk_body(
275 node: TsNode<'_>,
276 source: &[u8],
277 path: &Path,
278 file_id: &str,
279 stem: &str,
280 owner_id: &str,
281 protocols: &HashSet<String>,
282 fragment: &mut ExtractionFragment,
283 ) {
284 for i in 0..node.child_count() {
285 if let Some(child) = node.child(i) {
286 match child.kind() {
287 "function_declaration"
288 | "init_declaration"
289 | "deinit_declaration"
290 | "subscript_declaration" => {
291 Self::extract_function(
292 child, source, path, file_id, stem, owner_id, protocols, fragment,
293 );
294 }
295 _ => {}
296 }
297 }
298 }
299 }
300
301 #[allow(clippy::too_many_arguments)]
302 fn extract_function(
303 node: TsNode<'_>,
304 source: &[u8],
305 path: &Path,
306 file_id: &str,
307 stem: &str,
308 owner_id: &str,
309 _protocols: &HashSet<String>,
310 fragment: &mut ExtractionFragment,
311 ) {
312 let func_name = match node.kind() {
313 "deinit_declaration" => "deinit".to_string(),
314 "subscript_declaration" => "subscript".to_string(),
315 _ => {
316 let mut found = None;
317 for i in 0..node.child_count() {
318 if let Some(child) = node.child(i) {
319 if child.kind() == "simple_identifier" {
320 let t = sw_text(source, &child);
321 if !t.is_empty() && t != "func" && t != "init" {
322 found = Some(t);
323 break;
324 }
325 }
326 }
327 }
328 match found {
329 Some(n) => n,
330 None => return,
331 }
332 }
333 };
334 let func_id = make_id(&[stem, &func_name, "()"]);
335 let fn_file_type = if owner_id == file_id {
336 "function"
337 } else {
338 "method"
339 };
340 add_node_if_missing(
341 fragment,
342 sw_node(
343 func_id.clone(),
344 format!("{}()", func_name),
345 fn_file_type,
346 path,
347 ),
348 );
349 fragment.edges.push(Edge {
350 source: owner_id.to_string(),
351 target: func_id.clone(),
352 relation: "method".to_string(),
353 confidence: "EXTRACTED".to_string(),
354 source_file: Some(path.to_string_lossy().to_string()),
355 weight: 1.0,
356 context: None,
357 });
358 fragment.edges.push(Edge {
359 source: file_id.to_string(),
360 target: func_id.clone(),
361 relation: "contains".to_string(),
362 confidence: "EXTRACTED".to_string(),
363 source_file: Some(path.to_string_lossy().to_string()),
364 weight: 1.0,
365 context: None,
366 });
367
368 for i in 0..node.child_count() {
370 if let Some(child) = node.child(i) {
371 if child.kind() == "type_annotation" {
372 Self::emit_return_type_ref(&child, source, path, &func_id, fragment, stem);
374 }
375 }
376 }
377
378 for i in 0..node.child_count() {
380 if let Some(child) = node.child(i) {
381 if child.kind() == "parameter" {
382 Self::extract_swift_param(&child, source, path, &func_id, fragment, stem);
383 } else if child.kind() == "function_value_parameters"
384 || child.kind() == "parameter_clause"
385 {
386 for j in 0..child.child_count() {
387 if let Some(param) = child.child(j) {
388 if param.kind() == "parameter" {
389 Self::extract_swift_param(
390 ¶m, source, path, &func_id, fragment, stem,
391 );
392 }
393 }
394 }
395 }
396 }
397 }
398
399 for i in 0..node.child_count() {
401 if let Some(child) = node.child(i) {
402 if child.kind() == "function_body" || child.kind() == "code_block" {
403 Self::walk_calls_sw(child, source, path, &func_id, fragment, stem);
404 }
405 }
406 }
407 }
408
409 fn emit_return_type_ref(
410 annot: &TsNode<'_>,
411 source: &[u8],
412 path: &Path,
413 func_id: &str,
414 fragment: &mut ExtractionFragment,
415 stem: &str,
416 ) {
417 for i in 0..annot.child_count() {
418 if let Some(child) = annot.child(i) {
419 if child.kind() == "user_type" {
420 if let Some(name_n) = child.child(0) {
421 if name_n.kind() == "type_identifier" {
422 let name = sw_text(source, &name_n);
423 if !name.is_empty() {
424 let id = make_id(&[stem, &name]);
425 add_node_if_missing(
426 fragment,
427 sw_node(id.clone(), name.clone(), "code", path),
428 );
429 fragment.edges.push(Edge {
430 source: func_id.to_string(),
431 target: id,
432 relation: "references".to_string(),
433 confidence: "EXTRACTED".to_string(),
434 source_file: Some(path.to_string_lossy().to_string()),
435 weight: 1.0,
436 context: Some("return_type".to_string()),
437 });
438 if name_n.child_count() == 0 {
440 for j in 0..child.child_count() {
442 if let Some(ta) = child.child(j) {
443 if ta.kind() == "type_arguments" {
444 Self::emit_generic_args_sw(
445 &ta, source, path, func_id, fragment, stem,
446 );
447 }
448 }
449 }
450 }
451 }
452 }
453 }
454 }
455 }
456 }
457 }
458
459 fn emit_generic_args_sw(
460 args: &TsNode<'_>,
461 source: &[u8],
462 path: &Path,
463 func_id: &str,
464 fragment: &mut ExtractionFragment,
465 stem: &str,
466 ) {
467 for i in 0..args.child_count() {
468 if let Some(arg) = args.child(i) {
469 if arg.kind() == "type_identifier" {
470 let name = sw_text(source, &arg);
471 if !name.is_empty() {
472 let id = make_id(&[stem, &name]);
473 add_node_if_missing(fragment, sw_node(id.clone(), name, "code", path));
474 fragment.edges.push(Edge {
475 source: func_id.to_string(),
476 target: id,
477 relation: "references".to_string(),
478 confidence: "EXTRACTED".to_string(),
479 source_file: Some(path.to_string_lossy().to_string()),
480 weight: 1.0,
481 context: Some("generic_arg".to_string()),
482 });
483 }
484 }
485 }
486 }
487 }
488
489 fn extract_swift_param(
490 param: &TsNode<'_>,
491 source: &[u8],
492 path: &Path,
493 func_id: &str,
494 fragment: &mut ExtractionFragment,
495 stem: &str,
496 ) {
497 for i in 0..param.child_count() {
498 if let Some(child) = param.child(i) {
499 if child.kind() == "type_annotation" {
500 for j in 0..child.child_count() {
501 if let Some(t) = child.child(j) {
502 if t.kind() == "user_type" {
503 if let Some(name_n) = t.child(0) {
504 if name_n.kind() == "type_identifier" {
505 let name = sw_text(source, &name_n);
506 if !name.is_empty() {
507 let id = make_id(&[stem, &name]);
508 add_node_if_missing(
509 fragment,
510 sw_node(id.clone(), name.clone(), "code", path),
511 );
512 fragment.edges.push(Edge {
513 source: func_id.to_string(),
514 target: id.clone(),
515 relation: "references".to_string(),
516 confidence: "EXTRACTED".to_string(),
517 source_file: Some(
518 path.to_string_lossy().to_string(),
519 ),
520 weight: 1.0,
521 context: Some("parameter_type".to_string()),
522 });
523 for k in 0..t.child_count() {
525 if let Some(ta) = t.child(k) {
526 if ta.kind() == "type_arguments" {
527 Self::emit_generic_args_sw(
528 &ta, source, path, func_id, fragment,
529 stem,
530 );
531 }
532 }
533 }
534 }
535 }
536 }
537 }
538 }
539 }
540 }
541 }
542 }
543 }
544
545 fn walk_calls_sw(
546 node: TsNode<'_>,
547 source: &[u8],
548 path: &Path,
549 caller_id: &str,
550 fragment: &mut ExtractionFragment,
551 stem: &str,
552 ) {
553 if node.kind() == "call_expression" {
554 if let Some(func) = node.child(0) {
555 let callee_name = match func.kind() {
556 "navigation_expression" | "member_access_expression" => {
557 let mut name = String::new();
559 for i in 0..func.child_count() {
560 if let Some(child) = func.child(i) {
561 if child.kind() == "simple_identifier" {
562 name = sw_text(source, &child);
563 }
564 }
565 }
566 name
567 }
568 "simple_identifier" => sw_text(source, &func),
569 _ => String::new(),
570 };
571 if !callee_name.is_empty() {
572 let callee_id = make_id(&[stem, &callee_name, "()"]);
573 if fragment.nodes.iter().any(|n| n.id == callee_id) {
574 let already = fragment.edges.iter().any(|e| {
575 e.relation == "calls" && e.source == caller_id && e.target == callee_id
576 });
577 if !already && caller_id != callee_id.as_str() {
578 fragment.edges.push(Edge {
579 source: caller_id.to_string(),
580 target: callee_id,
581 relation: "calls".to_string(),
582 confidence: "EXTRACTED".to_string(),
583 source_file: Some(path.to_string_lossy().to_string()),
584 weight: 1.0,
585 context: Some("call".to_string()),
586 });
587 }
588 }
589 }
590 }
591 }
592 for i in 0..node.child_count() {
593 if let Some(child) = node.child(i) {
594 Self::walk_calls_sw(child, source, path, caller_id, fragment, stem);
595 }
596 }
597 }
598}
599
600impl LanguageExtractor for TsSwiftExtractor {
601 fn file_extensions(&self) -> Vec<&'static str> {
602 vec!["swift"]
603 }
604 fn extract(&self, source: &[u8], path: &Path) -> Result<ExtractionFragment> {
605 Self::extract(source, path)
606 }
607 fn resolve_imports(&self, _imports: &[ImportNode]) -> Vec<Edge> {
608 vec![]
609 }
610 fn collect_type_refs(&self, _fragment: &mut ExtractionFragment) {}
611}