1use std::{
2 collections::HashMap,
3 path::{Path, PathBuf},
4};
5
6use gitcortex_core::{
7 error::{GitCortexError, Result},
8 graph::{Edge, Node, NodeId, NodeMetadata, Span},
9 schema::{EdgeConfidence, EdgeKind, NodeKind, Visibility},
10};
11use tree_sitter::{Node as TsNode, Parser};
12
13use super::{capture_definition, LanguageParser, ParseResult};
14
15pub struct GoParser {
16 language: tree_sitter::Language,
17}
18
19impl GoParser {
20 pub fn new() -> Self {
21 Self {
22 language: tree_sitter_go::LANGUAGE.into(),
23 }
24 }
25}
26
27impl Default for GoParser {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33impl LanguageParser for GoParser {
34 fn extensions(&self) -> &[&str] {
35 &["go"]
36 }
37
38 fn parse(&self, path: &Path, source: &str) -> Result<ParseResult> {
39 let mut parser = Parser::new();
40 parser
41 .set_language(&self.language)
42 .map_err(|e| GitCortexError::Parse {
43 file: path.to_owned(),
44 message: e.to_string(),
45 })?;
46
47 let tree = parser
48 .parse(source, None)
49 .ok_or_else(|| GitCortexError::Parse {
50 file: path.to_owned(),
51 message: "tree-sitter returned no parse tree".into(),
52 })?;
53
54 let mut visitor = FileVisitor::new(path, source, tree.root_node());
55 visitor.collect_names(tree.root_node());
56 visitor.visit_source_file(tree.root_node());
57 visitor.collect_imports(tree.root_node());
58 visitor.collect_interface_assertions(tree.root_node());
59
60 Ok(ParseResult {
61 nodes: visitor.nodes,
62 edges: visitor.edges,
63 deferred_calls: visitor.deferred_calls,
64 deferred_uses: visitor.deferred_uses,
65 deferred_implements: visitor.deferred_implements,
66 deferred_imports: visitor.deferred_imports,
67 deferred_inherits: visitor.deferred_inherits,
68 deferred_throws: Vec::new(),
69 deferred_annotated: Vec::new(),
70 })
71 }
72}
73
74struct FileVisitor<'src> {
77 source: &'src [u8],
78 file: PathBuf,
79 package_id: NodeId,
81 nodes: Vec<Node>,
82 edges: Vec<Edge>,
83 type_index: HashMap<String, NodeId>,
85 fn_index: HashMap<String, NodeId>,
87 deferred_calls: Vec<(NodeId, String, u32)>,
88 deferred_uses: Vec<(NodeId, String)>,
89 deferred_implements: Vec<(NodeId, String)>,
90 deferred_imports: Vec<(NodeId, String)>,
91 deferred_inherits: Vec<(NodeId, String)>,
92}
93
94impl<'src> FileVisitor<'src> {
95 fn new(file: &Path, source: &'src str, root: TsNode<'_>) -> Self {
96 let package_id = NodeId::new();
97 let package_name = {
99 let mut c = root.walk();
100 let pkg_clause: Vec<TsNode<'_>> = root.named_children(&mut c).collect();
101 let name = pkg_clause
102 .iter()
103 .find(|n| n.kind() == "package_clause")
104 .and_then(|pc| {
105 let mut cc = pc.walk();
106 let ids: Vec<TsNode<'_>> = pc.named_children(&mut cc).collect();
107 ids.into_iter()
108 .find(|n| n.kind() == "package_identifier")
109 .map(|n| n.utf8_text(source.as_bytes()).unwrap_or("main").to_owned())
110 })
111 .unwrap_or_else(|| {
112 file.file_stem()
113 .and_then(|s| s.to_str())
114 .unwrap_or("main")
115 .to_owned()
116 });
117 name
118 };
119 let package_node = Node {
120 id: package_id.clone(),
121 qualified_name: package_name.clone(),
122 kind: NodeKind::Module,
123 name: package_name,
124 file: file.to_owned(),
125 span: Span {
126 start_line: 1,
127 end_line: 1,
128 },
129 metadata: NodeMetadata {
130 loc: source.lines().count() as u32,
131 visibility: Visibility::Pub,
132 is_async: false,
133 is_unsafe: false,
134 ..Default::default()
135 },
136 };
137 let nodes = vec![package_node];
138 Self {
139 source: source.as_bytes(),
140 file: file.to_owned(),
141 package_id,
142 nodes,
143 edges: Vec::new(),
144 type_index: HashMap::new(),
145 fn_index: HashMap::new(),
146 deferred_calls: Vec::new(),
147 deferred_uses: Vec::new(),
148 deferred_implements: Vec::new(),
149 deferred_imports: Vec::new(),
150 deferred_inherits: Vec::new(),
151 }
152 }
153
154 fn text<'t>(&self, node: TsNode<'t>) -> &'src str {
155 node.utf8_text(self.source).unwrap_or("")
156 }
157
158 fn span(node: TsNode<'_>) -> Span {
159 Span {
160 start_line: node.start_position().row as u32 + 1,
161 end_line: node.end_position().row as u32 + 1,
162 }
163 }
164
165 fn visibility(name: &str) -> Visibility {
167 if name
168 .chars()
169 .next()
170 .map(|c| c.is_uppercase())
171 .unwrap_or(false)
172 {
173 Visibility::Pub
174 } else {
175 Visibility::Private
176 }
177 }
178
179 fn qualified(scope: &[String], name: &str) -> String {
180 if scope.is_empty() {
181 name.to_owned()
182 } else {
183 format!("{}.{name}", scope.join("."))
184 }
185 }
186
187 fn make_node(
188 &self,
189 id: NodeId,
190 kind: NodeKind,
191 name: String,
192 scope: &[String],
193 ts_node: TsNode<'_>,
194 ) -> Node {
195 Node {
196 id,
197 qualified_name: Self::qualified(scope, &name),
198 kind,
199 name: name.clone(),
200 file: self.file.clone(),
201 span: Self::span(ts_node),
202 metadata: NodeMetadata {
203 loc: (ts_node.end_position().row - ts_node.start_position().row + 1) as u32,
204 visibility: Self::visibility(&name),
205 is_async: false,
206 is_unsafe: false,
207 definition: capture_definition(self.source, ts_node),
208 ..Default::default()
209 },
210 }
211 }
212
213 fn collect_names(&mut self, node: TsNode<'_>) {
216 let mut cursor = node.walk();
217 let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
218 for child in children {
219 match child.kind() {
220 "function_declaration" => {
221 if let Some(name_node) = child.child_by_field_name("name") {
222 let name = self.text(name_node).to_owned();
223 self.fn_index.entry(name).or_default();
224 }
225 }
226 "method_declaration" => {
227 if let Some(name_node) = child.child_by_field_name("name") {
228 let name = self.text(name_node).to_owned();
229 self.fn_index.entry(name).or_default();
230 }
231 }
232 "type_declaration" => {
233 self.collect_type_decl_names(child);
234 }
235 _ => {}
236 }
237 }
238 }
239
240 fn collect_type_decl_names(&mut self, decl: TsNode<'_>) {
241 let mut cursor = decl.walk();
242 for spec in decl.named_children(&mut cursor) {
243 if spec.kind() != "type_spec" {
244 continue;
245 }
246 if let Some(name_node) = spec.child_by_field_name("name") {
247 let name = self.text(name_node).to_owned();
248 if let Some(type_node) = spec.child_by_field_name("type") {
249 if matches!(type_node.kind(), "struct_type" | "interface_type") {
250 self.type_index.entry(name).or_default();
251 }
252 }
253 }
254 }
255 }
256
257 fn visit_source_file(&mut self, node: TsNode<'_>) {
260 let mut cursor = node.walk();
261 let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
262 for child in children {
263 self.visit_top_level(child);
264 }
265 }
266
267 fn visit_top_level(&mut self, node: TsNode<'_>) {
268 match node.kind() {
269 "function_declaration" => self.visit_function(node, &[]),
270 "method_declaration" => self.visit_method(node),
271 "type_declaration" => self.visit_type_decl(node),
272 "const_declaration" => self.visit_const_decl(node),
273 _ => {}
274 }
275 }
276
277 fn visit_function(&mut self, node: TsNode<'_>, scope: &[String]) {
278 let Some(name_node) = node.child_by_field_name("name") else {
279 return;
280 };
281 let name = self.text(name_node).to_owned();
282 let id = self
283 .fn_index
284 .get(&name)
285 .cloned()
286 .unwrap_or_else(NodeId::new);
287 let mut graph_node =
288 self.make_node(id.clone(), NodeKind::Function, name.clone(), scope, node);
289
290 if name == "init" || name == "main" {
292 graph_node.metadata.is_static = true;
293 }
294
295 graph_node.metadata.generic_bounds = self.collect_generic_bounds(node);
298
299 if let Some(body) = node.child_by_field_name("body") {
300 graph_node.metadata.lld.complexity = Some(super::cyclomatic_complexity(
301 body,
302 &super::complexity::go_decision,
303 ));
304 }
305
306 self.nodes.push(graph_node);
307
308 self.extract_fn_type_uses(node, &id);
309
310 if let Some(body) = node.child_by_field_name("body") {
311 self.collect_calls(body, &id);
312 }
313 }
314
315 fn visit_method(&mut self, node: TsNode<'_>) {
316 let Some(name_node) = node.child_by_field_name("name") else {
317 return;
318 };
319 let name = self.text(name_node).to_owned();
320
321 let receiver_type = self.receiver_type(node);
322 let scope: Vec<String> = receiver_type.into_iter().collect();
323
324 let container_id = scope.first().and_then(|t| self.type_index.get(t).cloned());
325 let id = self
326 .fn_index
327 .get(&name)
328 .cloned()
329 .unwrap_or_else(NodeId::new);
330 let mut graph_node = self.make_node(id.clone(), NodeKind::Method, name, &scope, node);
331
332 if let Some(body) = node.child_by_field_name("body") {
333 graph_node.metadata.lld.complexity = Some(super::cyclomatic_complexity(
334 body,
335 &super::complexity::go_decision,
336 ));
337 }
338
339 if let Some(cid) = container_id {
340 self.edges.push(Edge {
341 src: cid,
342 dst: id.clone(),
343 kind: EdgeKind::Contains,
344 line: None,
345 confidence: EdgeConfidence::Extracted,
346 });
347 }
348 self.nodes.push(graph_node);
349
350 self.extract_fn_type_uses(node, &id);
351
352 if let Some(body) = node.child_by_field_name("body") {
353 self.collect_calls(body, &id);
354 }
355 }
356
357 fn receiver_type(&self, method_node: TsNode<'_>) -> Option<String> {
359 let recv = method_node.child_by_field_name("receiver")?;
360 let mut cursor = recv.walk();
361 for param in recv.named_children(&mut cursor) {
362 if param.kind() != "parameter_declaration" {
363 continue;
364 }
365 if let Some(type_node) = param.child_by_field_name("type") {
366 return match type_node.kind() {
367 "type_identifier" => Some(self.text(type_node).to_owned()),
368 "pointer_type" => {
369 let mut c = type_node.walk();
370 let result = type_node
371 .named_children(&mut c)
372 .find(|n| n.kind() == "type_identifier")
373 .map(|n| self.text(n).to_owned());
374 result
375 }
376 _ => None,
377 };
378 }
379 }
380 None
381 }
382
383 fn visit_type_decl(&mut self, decl: TsNode<'_>) {
384 let mut cursor = decl.walk();
385 let specs: Vec<TsNode<'_>> = decl.named_children(&mut cursor).collect();
386 for spec in specs {
387 if spec.kind() != "type_spec" {
388 continue;
389 }
390 let Some(name_node) = spec.child_by_field_name("name") else {
391 continue;
392 };
393 let name = self.text(name_node).to_owned();
394 let Some(type_node) = spec.child_by_field_name("type") else {
395 continue;
396 };
397
398 match type_node.kind() {
399 "struct_type" => {
400 let id = self
401 .type_index
402 .get(&name)
403 .cloned()
404 .unwrap_or_else(NodeId::new);
405 let mut graph_node =
406 self.make_node(id.clone(), NodeKind::Struct, name, &[], spec);
407 graph_node.metadata.generic_bounds = self.collect_generic_bounds(spec);
409 self.nodes.push(graph_node);
410 self.extract_struct_field_uses(type_node, &id);
412 }
413 "interface_type" => {
414 let id = self
415 .type_index
416 .get(&name)
417 .cloned()
418 .unwrap_or_else(NodeId::new);
419 let mut graph_node =
420 self.make_node(id.clone(), NodeKind::Interface, name, &[], spec);
421 graph_node.metadata.generic_bounds = self.collect_generic_bounds(spec);
423 self.nodes.push(graph_node);
424 self.extract_interface_methods(type_node, &id);
426 }
427 _ => {
428 let id = NodeId::new();
429 let graph_node = self.make_node(id, NodeKind::TypeAlias, name, &[], spec);
430 self.nodes.push(graph_node);
431 }
432 }
433 }
434 }
435
436 fn visit_const_decl(&mut self, node: TsNode<'_>) {
437 let mut cursor = node.walk();
438 for spec in node.named_children(&mut cursor) {
439 if spec.kind() != "const_spec" {
440 continue;
441 }
442 let Some(name_node) = spec.child_by_field_name("name") else {
443 continue;
444 };
445 let name = self.text(name_node).to_owned();
446 let id = NodeId::new();
447 let mut graph_node = self.make_node(id, NodeKind::Constant, name, &[], spec);
448 graph_node.metadata.is_const = true;
449 self.nodes.push(graph_node);
450 }
451 }
452
453 fn collect_imports(&mut self, node: TsNode<'_>) {
456 let mut cursor = node.walk();
457 let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
458 for child in children {
459 if child.kind() != "import_declaration" {
460 continue;
461 }
462 let mut c = child.walk();
464 let decl_children: Vec<TsNode<'_>> = child.named_children(&mut c).collect();
465 for dc in decl_children {
466 match dc.kind() {
467 "import_spec" => self.record_import_spec(dc),
468 "import_spec_list" => {
469 let mut cc = dc.walk();
470 let specs: Vec<TsNode<'_>> = dc.named_children(&mut cc).collect();
471 for spec in specs {
472 if spec.kind() == "import_spec" {
473 self.record_import_spec(spec);
474 }
475 }
476 }
477 _ => {}
478 }
479 }
480 }
481 }
482
483 fn record_import_spec(&mut self, spec: TsNode<'_>) {
484 let alias = spec
486 .child_by_field_name("name")
487 .map(|n| self.text(n).to_owned());
488
489 if alias.as_deref() == Some("_") {
491 return;
492 }
493
494 let pkg_name = if let Some(alias) = alias {
495 alias
496 } else if let Some(path_node) = spec.child_by_field_name("path") {
497 let raw = self.text(path_node).trim_matches('"').trim_matches('\'');
499 raw.split('/').next_back().unwrap_or(raw).to_owned()
500 } else {
501 return;
502 };
503
504 self.deferred_imports
505 .push((self.package_id.clone(), pkg_name));
506 }
507
508 fn collect_interface_assertions(&mut self, node: TsNode<'_>) {
512 let mut cursor = node.walk();
513 let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
514 for child in children {
515 if child.kind() != "var_declaration" {
516 continue;
517 }
518 let mut c = child.walk();
519 let specs: Vec<TsNode<'_>> = child.named_children(&mut c).collect();
520 for spec in specs {
521 if spec.kind() != "var_spec" {
522 continue;
523 }
524 let mut cc = spec.walk();
525 let spec_children: Vec<TsNode<'_>> = spec.named_children(&mut cc).collect();
526 if spec_children.len() < 3 {
528 continue;
529 }
530 if spec_children[0].kind() != "identifier" || self.text(spec_children[0]) != "_" {
531 continue;
532 }
533 if spec_children[1].kind() != "type_identifier" {
534 continue;
535 }
536 let interface_name = self.text(spec_children[1]).to_owned();
537 let value = spec_children[2];
539 let mut candidates = Vec::new();
540 self.collect_candidate_type_names(value, &mut candidates);
541 for struct_name in candidates {
542 if let Some(struct_id) = self.type_index.get(&struct_name).cloned() {
543 self.deferred_implements
544 .push((struct_id, interface_name.clone()));
545 }
546 }
547 }
548 }
549 }
550
551 fn collect_candidate_type_names(&self, node: TsNode<'_>, out: &mut Vec<String>) {
553 match node.kind() {
554 "identifier" | "type_identifier" => {
555 let name = self.text(node).to_owned();
556 if name != "nil" && !is_builtin_go_type(&name) {
557 out.push(name);
558 }
559 }
560 _ => {
561 let mut c = node.walk();
562 for child in node.named_children(&mut c) {
563 self.collect_candidate_type_names(child, out);
564 }
565 }
566 }
567 }
568
569 fn extract_fn_type_uses(&mut self, fn_node: TsNode<'_>, fn_id: &NodeId) {
573 if let Some(params) = fn_node.child_by_field_name("parameters") {
575 let mut c = params.walk();
576 let param_list: Vec<TsNode<'_>> = params.named_children(&mut c).collect();
577 for param in param_list {
578 if param.kind() == "parameter_declaration"
579 || param.kind() == "variadic_parameter_declaration"
580 {
581 if let Some(type_node) = param.child_by_field_name("type") {
582 for name in self.collect_type_idents(type_node) {
583 self.deferred_uses.push((fn_id.clone(), name));
584 }
585 }
586 }
587 }
588 }
589 if let Some(result) = fn_node.child_by_field_name("result") {
591 match result.kind() {
592 "parameter_list" => {
593 let mut c = result.walk();
594 let ret_params: Vec<TsNode<'_>> = result.named_children(&mut c).collect();
595 for rp in ret_params {
596 if rp.kind() == "parameter_declaration" {
597 if let Some(type_node) = rp.child_by_field_name("type") {
598 for name in self.collect_type_idents(type_node) {
599 self.deferred_uses.push((fn_id.clone(), name));
600 }
601 }
602 }
603 }
604 }
605 _ => {
607 for name in self.collect_type_idents(result) {
608 self.deferred_uses.push((fn_id.clone(), name));
609 }
610 }
611 }
612 }
613 }
614
615 fn extract_struct_field_uses(&mut self, struct_type: TsNode<'_>, struct_id: &NodeId) {
618 let mut tw = struct_type.walk();
619 let top: Vec<TsNode<'_>> = struct_type.named_children(&mut tw).collect();
620 let Some(field_list) = top
621 .iter()
622 .find(|n| n.kind() == "field_declaration_list")
623 .copied()
624 else {
625 return;
626 };
627 let mut c = field_list.walk();
628 let fields: Vec<TsNode<'_>> = field_list.named_children(&mut c).collect();
629 for field in fields {
630 if field.kind() == "field_declaration" {
631 let has_name = field.child_by_field_name("name").is_some();
635 if let Some(type_node) = field.child_by_field_name("type") {
636 let type_names = self.collect_type_idents(type_node);
637 for name in &type_names {
638 self.deferred_uses.push((struct_id.clone(), name.clone()));
639 }
640 if !has_name {
642 for name in type_names {
643 self.deferred_inherits.push((struct_id.clone(), name));
644 }
645 }
646 }
647 }
648 }
649 }
650
651 fn extract_interface_methods(&mut self, interface_type: TsNode<'_>, iface_id: &NodeId) {
653 let mut c = interface_type.walk();
654 let children: Vec<TsNode<'_>> = interface_type.named_children(&mut c).collect();
655 for child in children {
656 if child.kind() == "method_elem" {
658 let mut cc = child.walk();
659 let method_children: Vec<TsNode<'_>> = child.named_children(&mut cc).collect();
660 let Some(name_node) = method_children
662 .iter()
663 .find(|n| n.kind() == "field_identifier")
664 else {
665 continue;
666 };
667 let name = self.text(*name_node).to_owned();
668 let id = NodeId::new();
669 let graph_node = self.make_node(id.clone(), NodeKind::Method, name, &[], child);
670 self.edges.push(Edge {
671 src: iface_id.clone(),
672 dst: id.clone(),
673 kind: EdgeKind::Contains,
674 line: None,
675 confidence: EdgeConfidence::Extracted,
676 });
677 self.nodes.push(graph_node);
678 }
679 }
680 }
681
682 fn collect_generic_bounds(&self, node: TsNode<'_>) -> Vec<String> {
687 let Some(type_params) = node.child_by_field_name("type_parameters") else {
688 return Vec::new();
689 };
690 let mut bounds = Vec::new();
691 let mut cursor = type_params.walk();
692 for child in type_params.named_children(&mut cursor) {
693 if child.kind() == "type_parameter_declaration" {
696 let name = child
697 .child_by_field_name("name")
698 .map(|n| self.text(n))
699 .unwrap_or("");
700 let constraint = child
701 .child_by_field_name("type")
702 .map(|n| self.text(n))
703 .unwrap_or("");
704 if !name.is_empty() {
705 let bound = if constraint.is_empty() {
706 name.to_owned()
707 } else {
708 format!("{name} {constraint}")
709 };
710 bounds.push(bound);
711 }
712 }
713 }
714 bounds
715 }
716
717 fn collect_type_idents(&self, node: TsNode<'_>) -> Vec<String> {
719 let mut names = Vec::new();
720 self.walk_type_idents(node, &mut names);
721 names
722 }
723
724 fn walk_type_idents(&self, node: TsNode<'_>, out: &mut Vec<String>) {
725 match node.kind() {
726 "type_identifier" => {
727 let name = self.text(node).to_owned();
728 if !is_builtin_go_type(&name) {
729 out.push(name);
730 }
731 }
732 _ => {
733 let mut c = node.walk();
734 for child in node.named_children(&mut c) {
735 self.walk_type_idents(child, out);
736 }
737 }
738 }
739 }
740
741 fn collect_calls(&mut self, node: TsNode<'_>, caller_id: &NodeId) {
744 let mut cursor = node.walk();
745 let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
746 for child in children {
747 if child.kind() == "call_expression" {
748 if let Some(callee) = self.callee_name(child) {
749 let line = child.start_position().row as u32 + 1;
750 self.record_call(caller_id.clone(), callee, line);
751 }
752 if let Some(args) = child.child_by_field_name("arguments") {
753 self.collect_calls(args, caller_id);
754 }
755 } else if child.kind() == "go_statement" {
756 if let Some(call) = child.named_child(0) {
758 if call.kind() == "call_expression" {
759 if let Some(callee) = self.callee_name(call) {
760 let line = call.start_position().row as u32 + 1;
762 self.deferred_calls.push((caller_id.clone(), callee, line));
763 }
764 }
765 }
766 } else {
767 self.collect_calls(child, caller_id);
768 }
769 }
770 }
771
772 fn callee_name(&self, call_expr: TsNode<'_>) -> Option<String> {
773 let func = call_expr.child_by_field_name("function")?;
774 match func.kind() {
775 "identifier" => Some(self.text(func).to_owned()),
776 "selector_expression" => func
777 .child_by_field_name("field")
778 .map(|n| self.text(n).to_owned()),
779 _ => None,
780 }
781 }
782
783 fn record_call(&mut self, caller_id: NodeId, callee_name: String, line: u32) {
784 if callee_name.is_empty() {
785 return;
786 }
787 if let Some(callee_id) = self.fn_index.get(&callee_name).cloned() {
788 let edge = Edge::call(caller_id, callee_id, line);
789 if !self.edges.contains(&edge) {
790 self.edges.push(edge);
791 }
792 } else if !self
793 .deferred_calls
794 .iter()
795 .any(|(c, n, _)| c == &caller_id && n == &callee_name)
796 {
797 self.deferred_calls.push((caller_id, callee_name, line));
798 }
799 }
800}
801
802fn is_builtin_go_type(name: &str) -> bool {
804 matches!(
805 name,
806 "bool"
807 | "byte"
808 | "complex64"
809 | "complex128"
810 | "error"
811 | "float32"
812 | "float64"
813 | "int"
814 | "int8"
815 | "int16"
816 | "int32"
817 | "int64"
818 | "rune"
819 | "string"
820 | "uint"
821 | "uint8"
822 | "uint16"
823 | "uint32"
824 | "uint64"
825 | "uintptr"
826 | "any"
827 | "comparable"
828 )
829}
830
831#[cfg(test)]
834mod tests {
835 use super::GoParser;
836 use crate::parser::LanguageParser;
837 use gitcortex_core::schema::{EdgeKind, NodeKind};
838 use std::path::Path;
839
840 fn parse(
841 src: &str,
842 ) -> (
843 Vec<gitcortex_core::graph::Node>,
844 Vec<gitcortex_core::graph::Edge>,
845 ) {
846 let r = GoParser::new().parse(Path::new("test.go"), src).unwrap();
847 (r.nodes, r.edges)
848 }
849
850 #[allow(clippy::type_complexity)]
851 fn parse_full(
852 src: &str,
853 ) -> (
854 Vec<gitcortex_core::graph::Node>,
855 Vec<gitcortex_core::graph::Edge>,
856 Vec<(gitcortex_core::graph::NodeId, String, u32)>,
857 Vec<(gitcortex_core::graph::NodeId, String)>,
858 Vec<(gitcortex_core::graph::NodeId, String)>,
859 Vec<(gitcortex_core::graph::NodeId, String)>,
860 ) {
861 let r = GoParser::new().parse(Path::new("test.go"), src).unwrap();
862 (
863 r.nodes,
864 r.edges,
865 r.deferred_calls,
866 r.deferred_uses,
867 r.deferred_implements,
868 r.deferred_imports,
869 )
870 }
871
872 #[test]
873 fn parses_function() {
874 let src = "package main\nfunc Greet(name string) string { return name }";
875 let (nodes, _) = parse(src);
876 let fns: Vec<_> = nodes
877 .iter()
878 .filter(|n| n.kind == NodeKind::Function)
879 .collect();
880 assert_eq!(fns.len(), 1);
881 assert_eq!(fns[0].name, "Greet");
882 }
883
884 #[test]
885 fn parses_struct_and_method() {
886 let src = "package main\ntype Person struct { Name string }\nfunc (p *Person) Greet() string { return p.Name }";
887 let (nodes, edges) = parse(src);
888 let structs: Vec<_> = nodes
889 .iter()
890 .filter(|n| n.kind == NodeKind::Struct)
891 .collect();
892 let methods: Vec<_> = nodes
893 .iter()
894 .filter(|n| n.kind == NodeKind::Method)
895 .collect();
896 assert_eq!(structs.len(), 1);
897 assert_eq!(methods.len(), 1);
898 let contains: Vec<_> = edges
899 .iter()
900 .filter(|e| e.kind == EdgeKind::Contains)
901 .collect();
902 assert!(!contains.is_empty());
903 }
904
905 #[test]
906 fn parses_interface() {
907 let src = "package main\ntype Greeter interface { Greet() string }";
908 let (nodes, _) = parse(src);
909 let ifaces: Vec<_> = nodes
910 .iter()
911 .filter(|n| n.kind == NodeKind::Interface)
912 .collect();
913 assert_eq!(ifaces.len(), 1);
914 assert_eq!(ifaces[0].name, "Greeter");
915 }
916
917 #[test]
918 fn go_visibility_is_uppercase() {
919 let src = "package main\nfunc Exported() {}\nfunc unexported() {}";
920 let (nodes, _) = parse(src);
921 use gitcortex_core::schema::Visibility;
922 let exp = nodes.iter().find(|n| n.name == "Exported").unwrap();
923 let unexp = nodes.iter().find(|n| n.name == "unexported").unwrap();
924 assert_eq!(exp.metadata.visibility, Visibility::Pub);
925 assert_eq!(unexp.metadata.visibility, Visibility::Private);
926 }
927
928 #[test]
929 fn detects_call_edges() {
930 let src = "package main\nfunc Caller() { Callee() }\nfunc Callee() {}";
931 let (_, edges) = parse(src);
932 let calls: Vec<_> = edges.iter().filter(|e| e.kind == EdgeKind::Calls).collect();
933 assert_eq!(calls.len(), 1);
934 }
935
936 #[test]
937 fn package_node_is_emitted() {
938 let src = "package mypackage\nfunc Foo() {}";
939 let (nodes, _) = parse(src);
940 let modules: Vec<_> = nodes
941 .iter()
942 .filter(|n| n.kind == NodeKind::Module)
943 .collect();
944 assert_eq!(modules.len(), 1);
945 assert_eq!(modules[0].name, "mypackage");
946 }
947
948 #[test]
949 fn detects_import_declaration() {
950 let src = "package main\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n)\nfunc main() {}";
951 let (_, _, _, _, _, imports) = parse_full(src);
952 assert!(
953 imports.iter().any(|(_, n)| n == "fmt"),
954 "expected import 'fmt', got: {imports:?}"
955 );
956 assert!(
957 imports.iter().any(|(_, n)| n == "exec"),
958 "expected import 'exec' (last segment of os/exec), got: {imports:?}"
959 );
960 }
961
962 #[test]
963 fn detects_fn_type_uses() {
964 let src = "package main\ntype Request struct{}\ntype Response struct{}\nfunc Handle(req *Request) *Response { return nil }";
965 let (_, _, _, uses, _, _) = parse_full(src);
966 assert!(
967 uses.iter().any(|(_, n)| n == "Request"),
968 "expected Uses edge to Request, got: {uses:?}"
969 );
970 assert!(
971 uses.iter().any(|(_, n)| n == "Response"),
972 "expected Uses edge to Response, got: {uses:?}"
973 );
974 }
975
976 #[test]
977 fn detects_interface_assertion() {
978 let src = "package main\ntype Greeter interface { Greet() string }\ntype Person struct{}\nvar _ Greeter = (*Person)(nil)";
979 let (_, _, _, _, implements, _) = parse_full(src);
980 assert!(
981 implements.iter().any(|(_, n)| n == "Greeter"),
982 "expected Implements edge to Greeter, got: {implements:?}"
983 );
984 }
985
986 #[test]
987 fn captures_interface_methods() {
988 let src = "package main\ntype Greeter interface { Greet() string\nGetName() string }";
989 let (nodes, edges) = parse(src);
990 let methods: Vec<_> = nodes
991 .iter()
992 .filter(|n| n.kind == NodeKind::Method)
993 .collect();
994 assert_eq!(methods.len(), 2, "expected 2 interface method specs");
995 let contains: Vec<_> = edges
996 .iter()
997 .filter(|e| e.kind == EdgeKind::Contains)
998 .collect();
999 assert_eq!(
1000 contains.len(),
1001 2,
1002 "expected 2 Contains edges from interface to methods"
1003 );
1004 }
1005}