Skip to main content

codesynapse_core/ts_extract/
cpp.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 TsCppExtractor;
9impl TsCppExtractor {
10    pub fn extract(source: &[u8], path: &Path) -> Result<ExtractionFragment> {
11        let (file_id, _, file_node) = make_file_node(path);
12        let mut fragment = ExtractionFragment {
13            nodes: vec![file_node],
14            edges: vec![],
15        };
16        let lang = tree_sitter_cpp::LANGUAGE.into();
17
18        // Class declarations
19        let class_query = r#"
20            (class_specifier
21                name: (type_identifier) @class.name
22            )
23        "#;
24        if let Ok(mut captures) = run_query_named(source, &lang, class_query) {
25            let names = captures.remove("class.name").unwrap_or_default();
26            for name in &names {
27                let class_id = make_id(&[&file_id, name]);
28                add_node_if_missing(
29                    &mut fragment,
30                    Node {
31                        id: class_id.clone(),
32                        label: name.to_string(),
33                        file_type: "class".to_string(),
34                        source_file: path.to_string_lossy().to_string(),
35                        source_location: None,
36                        community: None,
37                        rationale: None,
38                        docstring: None,
39                        metadata: HashMap::new(),
40                    },
41                );
42                add_contains_edge(&mut fragment, &file_id, class_id, path);
43            }
44        }
45
46        // Struct declarations
47        let struct_query = r#"
48            (struct_specifier
49                name: (type_identifier) @struct.name
50            )
51        "#;
52        if let Ok(mut captures) = run_query_named(source, &lang, struct_query) {
53            let names = captures.remove("struct.name").unwrap_or_default();
54            for name in &names {
55                let id = make_id(&[&file_id, name]);
56                add_node_if_missing(
57                    &mut fragment,
58                    Node {
59                        id: id.clone(),
60                        label: name.to_string(),
61                        file_type: "struct".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                add_contains_edge(&mut fragment, &file_id, id, path);
71            }
72        }
73
74        // Class inheritance: base_clause inside class_specifier
75        let _inherits_query = r#"
76            (class_specifier
77                name: (type_identifier) @child.name
78                (base_class_clause
79                    (type_identifier) @base.name
80                )
81            )
82        "#;
83        // Since run_query_named doesn't associate captures within a match,
84        // we use a simple heuristic: class/struct with a base clause inherits from that base.
85        // We need a different approach - use the walker pattern.
86        Self::extract_inheritance(source, path, &file_id, &lang, &mut fragment);
87
88        // Top-level free functions (identifier declarator = not a member)
89        let fn_top_query = r#"
90            (function_definition
91                declarator: (function_declarator
92                    declarator: (identifier) @fn.name
93                )
94            )
95        "#;
96        if let Ok(mut captures) = run_query_named(source, &lang, fn_top_query) {
97            for name in captures.remove("fn.name").unwrap_or_default() {
98                let fn_id = make_id(&[&file_id, &name, "()"]);
99                add_node_if_missing(
100                    &mut fragment,
101                    Node {
102                        id: fn_id.clone(),
103                        label: format!("{}()", name),
104                        file_type: "function".to_string(),
105                        source_file: path.to_string_lossy().to_string(),
106                        source_location: None,
107                        community: None,
108                        rationale: None,
109                        docstring: None,
110                        metadata: HashMap::new(),
111                    },
112                );
113                add_contains_edge(&mut fragment, &file_id, fn_id, path);
114            }
115        }
116
117        // Member function definitions / declarations (field_identifier = inside a class body)
118        let fn_member_query = r#"
119            [
120                (function_definition
121                    declarator: (function_declarator
122                        declarator: (field_identifier) @method.name
123                    )
124                )
125                (field_declaration
126                    declarator: (function_declarator
127                        declarator: (field_identifier) @method.name
128                    )
129                )
130            ]
131        "#;
132        if let Ok(mut captures) = run_query_named(source, &lang, fn_member_query) {
133            for name in captures.remove("method.name").unwrap_or_default() {
134                let fn_id = make_id(&[&file_id, &name, "()"]);
135                add_node_if_missing(
136                    &mut fragment,
137                    Node {
138                        id: fn_id.clone(),
139                        label: format!("{}()", name),
140                        file_type: "method".to_string(),
141                        source_file: path.to_string_lossy().to_string(),
142                        source_location: None,
143                        community: None,
144                        rationale: None,
145                        docstring: None,
146                        metadata: HashMap::new(),
147                    },
148                );
149                add_contains_edge(&mut fragment, &file_id, fn_id, path);
150            }
151        }
152
153        // Preprocessor includes with context="import"
154        let include_query = r#"
155            [
156                (preproc_include path: (string_literal) @include.path)
157                (preproc_include path: (system_lib_string) @include.path)
158            ]
159        "#;
160        if let Ok(mut captures) = run_query_named(source, &lang, include_query) {
161            for p in captures.remove("include.path").unwrap_or_default() {
162                let inc = p
163                    .trim()
164                    .trim_matches('"')
165                    .trim_matches('<')
166                    .trim_matches('>');
167                let inc_id = make_id(&[&file_id, inc]);
168                add_node_if_missing(
169                    &mut fragment,
170                    Node {
171                        id: inc_id.clone(),
172                        label: inc.to_string(),
173                        file_type: "code".to_string(),
174                        source_file: path.to_string_lossy().to_string(),
175                        source_location: None,
176                        community: None,
177                        rationale: None,
178                        docstring: None,
179                        metadata: HashMap::new(),
180                    },
181                );
182                fragment.edges.push(Edge {
183                    source: file_id.clone(),
184                    target: inc_id,
185                    relation: "imports".to_string(),
186                    confidence: "EXTRACTED".to_string(),
187                    source_file: Some(path.to_string_lossy().to_string()),
188                    weight: 1.0,
189                    context: Some("import".to_string()),
190                });
191            }
192        }
193
194        Ok(fragment)
195    }
196
197    fn extract_inheritance(
198        source: &[u8],
199        path: &Path,
200        file_id: &str,
201        lang: &tree_sitter::Language,
202        fragment: &mut ExtractionFragment,
203    ) {
204        use tree_sitter::{Node as TsNode, Parser};
205
206        let mut parser = Parser::new();
207        if parser.set_language(lang).is_err() {
208            return;
209        }
210        let tree = match parser.parse(source, None) {
211            Some(t) => t,
212            None => return,
213        };
214
215        fn text(source: &[u8], node: &TsNode<'_>) -> String {
216            std::str::from_utf8(&source[node.start_byte()..node.end_byte()])
217                .unwrap_or("")
218                .trim()
219                .to_string()
220        }
221
222        fn walk(
223            node: TsNode<'_>,
224            source: &[u8],
225            path: &Path,
226            file_id: &str,
227            fragment: &mut ExtractionFragment,
228        ) {
229            let kind = node.kind();
230            if kind == "class_specifier" || kind == "struct_specifier" {
231                // Find name
232                let name_node = node.child_by_field_name("name");
233                if let Some(name_node) = name_node {
234                    let class_name = text(source, &name_node);
235                    let class_id = make_id(&[file_id, &class_name]);
236
237                    // Find base_class_clause
238                    for i in 0..node.child_count() {
239                        if let Some(child) = node.child(i) {
240                            if child.kind() == "base_class_clause" {
241                                // Find type_identifier children (the base classes)
242                                for j in 0..child.child_count() {
243                                    if let Some(base) = child.child(j) {
244                                        if base.kind() == "type_identifier" {
245                                            let base_name = text(source, &base);
246                                            let base_id = make_id(&[&base_name]);
247                                            add_node_if_missing(
248                                                fragment,
249                                                Node {
250                                                    id: base_id.clone(),
251                                                    label: base_name,
252                                                    file_type: "code".to_string(),
253                                                    source_file: path.to_string_lossy().to_string(),
254                                                    source_location: None,
255                                                    community: None,
256                                                    rationale: None,
257                                                    docstring: None,
258                                                    metadata: HashMap::new(),
259                                                },
260                                            );
261                                            let already = fragment.edges.iter().any(|e| {
262                                                e.relation == "inherits"
263                                                    && e.source == class_id
264                                                    && e.target == base_id
265                                            });
266                                            if !already {
267                                                fragment.edges.push(Edge {
268                                                    source: class_id.clone(),
269                                                    target: base_id,
270                                                    relation: "inherits".to_string(),
271                                                    confidence: "EXTRACTED".to_string(),
272                                                    source_file: Some(
273                                                        path.to_string_lossy().to_string(),
274                                                    ),
275                                                    weight: 1.0,
276                                                    context: None,
277                                                });
278                                            }
279                                        }
280                                    }
281                                }
282                            }
283                            // Walk body
284                            walk(child, source, path, file_id, fragment);
285                        }
286                    }
287                }
288                return;
289            }
290
291            for i in 0..node.child_count() {
292                if let Some(child) = node.child(i) {
293                    walk(child, source, path, file_id, fragment);
294                }
295            }
296        }
297
298        walk(tree.root_node(), source, path, file_id, fragment);
299    }
300}
301
302impl LanguageExtractor for TsCppExtractor {
303    fn file_extensions(&self) -> Vec<&'static str> {
304        vec!["cpp", "cxx", "hpp", "hxx", "cc"]
305    }
306    fn extract(&self, source: &[u8], path: &Path) -> Result<ExtractionFragment> {
307        Self::extract(source, path)
308    }
309    fn resolve_imports(&self, _imports: &[ImportNode]) -> Vec<Edge> {
310        vec![]
311    }
312    fn collect_type_refs(&self, _fragment: &mut ExtractionFragment) {}
313}