Skip to main content

agentic_codebase/parse/
csharp.rs

1//! C# parsing using tree-sitter.
2//!
3//! Extracts classes, interfaces, structs, enums, methods, properties, namespaces, usings.
4
5use std::path::Path;
6
7use crate::types::{AcbResult, CodeUnitType, Language, Visibility};
8
9use super::treesitter::{get_node_text, node_to_span};
10use super::{LanguageParser, RawCodeUnit};
11
12/// C# language parser.
13pub struct CSharpParser;
14
15impl Default for CSharpParser {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21impl CSharpParser {
22    /// Create a new C# parser.
23    pub fn new() -> Self {
24        Self
25    }
26
27    fn extract_from_node(
28        &self,
29        node: tree_sitter::Node,
30        source: &str,
31        file_path: &Path,
32        units: &mut Vec<RawCodeUnit>,
33        next_id: &mut u64,
34        parent_qname: &str,
35    ) {
36        let mut cursor = node.walk();
37        for child in node.children(&mut cursor) {
38            match child.kind() {
39                "class_declaration" => {
40                    self.extract_type_decl(
41                        child,
42                        source,
43                        file_path,
44                        units,
45                        next_id,
46                        parent_qname,
47                        CodeUnitType::Type,
48                    );
49                }
50                "struct_declaration" => {
51                    self.extract_type_decl(
52                        child,
53                        source,
54                        file_path,
55                        units,
56                        next_id,
57                        parent_qname,
58                        CodeUnitType::Type,
59                    );
60                }
61                "interface_declaration" => {
62                    self.extract_type_decl(
63                        child,
64                        source,
65                        file_path,
66                        units,
67                        next_id,
68                        parent_qname,
69                        CodeUnitType::Trait,
70                    );
71                }
72                "enum_declaration" => {
73                    self.extract_type_decl(
74                        child,
75                        source,
76                        file_path,
77                        units,
78                        next_id,
79                        parent_qname,
80                        CodeUnitType::Type,
81                    );
82                }
83                "record_declaration" => {
84                    self.extract_type_decl(
85                        child,
86                        source,
87                        file_path,
88                        units,
89                        next_id,
90                        parent_qname,
91                        CodeUnitType::Type,
92                    );
93                }
94                "namespace_declaration" | "file_scoped_namespace_declaration" => {
95                    self.extract_namespace(
96                        child,
97                        source,
98                        file_path,
99                        units,
100                        next_id,
101                        parent_qname,
102                    );
103                }
104                "method_declaration" | "constructor_declaration" => {
105                    if let Some(unit) =
106                        self.extract_method(child, source, file_path, parent_qname, next_id)
107                    {
108                        units.push(unit);
109                    }
110                }
111                "property_declaration" => {
112                    if let Some(unit) =
113                        self.extract_property(child, source, file_path, parent_qname, next_id)
114                    {
115                        units.push(unit);
116                    }
117                }
118                "using_directive" => {
119                    if let Some(unit) =
120                        self.extract_using(child, source, file_path, parent_qname, next_id)
121                    {
122                        units.push(unit);
123                    }
124                }
125                // Recurse into declaration_list (body of namespace/class)
126                "declaration_list" => {
127                    self.extract_from_node(
128                        child,
129                        source,
130                        file_path,
131                        units,
132                        next_id,
133                        parent_qname,
134                    );
135                }
136                _ => {}
137            }
138        }
139    }
140
141    #[allow(clippy::too_many_arguments)]
142    fn extract_type_decl(
143        &self,
144        node: tree_sitter::Node,
145        source: &str,
146        file_path: &Path,
147        units: &mut Vec<RawCodeUnit>,
148        next_id: &mut u64,
149        parent_qname: &str,
150        unit_type: CodeUnitType,
151    ) {
152        let name = match node.child_by_field_name("name") {
153            Some(n) => get_node_text(n, source).to_string(),
154            None => return,
155        };
156        let qname = cs_qname(parent_qname, &name);
157        let span = node_to_span(node);
158        let vis = extract_cs_visibility(node, source);
159
160        let id = *next_id;
161        *next_id += 1;
162
163        let mut unit = RawCodeUnit::new(
164            unit_type,
165            Language::CSharp,
166            name,
167            file_path.to_path_buf(),
168            span,
169        );
170        unit.temp_id = id;
171        unit.qualified_name = qname.clone();
172        unit.visibility = vis;
173        units.push(unit);
174
175        // Recurse into body
176        if let Some(body) = node.child_by_field_name("body") {
177            self.extract_from_node(body, source, file_path, units, next_id, &qname);
178        }
179    }
180
181    fn extract_namespace(
182        &self,
183        node: tree_sitter::Node,
184        source: &str,
185        file_path: &Path,
186        units: &mut Vec<RawCodeUnit>,
187        next_id: &mut u64,
188        parent_qname: &str,
189    ) {
190        let name = match node.child_by_field_name("name") {
191            Some(n) => get_node_text(n, source).to_string(),
192            None => return,
193        };
194        let qname = cs_qname(parent_qname, &name);
195        let span = node_to_span(node);
196
197        let id = *next_id;
198        *next_id += 1;
199
200        let mut unit = RawCodeUnit::new(
201            CodeUnitType::Module,
202            Language::CSharp,
203            name,
204            file_path.to_path_buf(),
205            span,
206        );
207        unit.temp_id = id;
208        unit.qualified_name = qname.clone();
209        unit.visibility = Visibility::Public;
210        units.push(unit);
211
212        // Recurse into namespace body (or file-scoped namespace children)
213        if let Some(body) = node.child_by_field_name("body") {
214            self.extract_from_node(body, source, file_path, units, next_id, &qname);
215        } else {
216            // File-scoped namespace: children are siblings
217            self.extract_from_node(node, source, file_path, units, next_id, &qname);
218        }
219    }
220
221    fn extract_method(
222        &self,
223        node: tree_sitter::Node,
224        source: &str,
225        file_path: &Path,
226        parent_qname: &str,
227        next_id: &mut u64,
228    ) -> Option<RawCodeUnit> {
229        let name_node = node.child_by_field_name("name")?;
230        let name = get_node_text(name_node, source).to_string();
231        let qname = cs_qname(parent_qname, &name);
232        let span = node_to_span(node);
233        let vis = extract_cs_visibility(node, source);
234
235        let id = *next_id;
236        *next_id += 1;
237
238        let is_test = has_cs_attribute(node, source, "Test")
239            || has_cs_attribute(node, source, "Fact")
240            || has_cs_attribute(node, source, "Theory")
241            || has_cs_attribute(node, source, "TestMethod");
242        let unit_type = if is_test {
243            CodeUnitType::Test
244        } else {
245            CodeUnitType::Function
246        };
247
248        let is_async = node_text_contains_modifier(node, source, "async");
249
250        let mut unit = RawCodeUnit::new(
251            unit_type,
252            Language::CSharp,
253            name,
254            file_path.to_path_buf(),
255            span,
256        );
257        unit.temp_id = id;
258        unit.qualified_name = qname;
259        unit.visibility = vis;
260        unit.is_async = is_async;
261
262        Some(unit)
263    }
264
265    fn extract_property(
266        &self,
267        node: tree_sitter::Node,
268        source: &str,
269        file_path: &Path,
270        parent_qname: &str,
271        next_id: &mut u64,
272    ) -> Option<RawCodeUnit> {
273        let name_node = node.child_by_field_name("name")?;
274        let name = get_node_text(name_node, source).to_string();
275        let qname = cs_qname(parent_qname, &name);
276        let span = node_to_span(node);
277        let vis = extract_cs_visibility(node, source);
278
279        let id = *next_id;
280        *next_id += 1;
281
282        let mut unit = RawCodeUnit::new(
283            CodeUnitType::Symbol,
284            Language::CSharp,
285            name,
286            file_path.to_path_buf(),
287            span,
288        );
289        unit.temp_id = id;
290        unit.qualified_name = qname;
291        unit.visibility = vis;
292
293        Some(unit)
294    }
295
296    fn extract_using(
297        &self,
298        node: tree_sitter::Node,
299        source: &str,
300        file_path: &Path,
301        parent_qname: &str,
302        next_id: &mut u64,
303    ) -> Option<RawCodeUnit> {
304        let text = get_node_text(node, source)
305            .trim_start_matches("using ")
306            .trim_start_matches("global ")
307            .trim_start_matches("static ")
308            .trim_end_matches(';')
309            .trim()
310            .to_string();
311        let span = node_to_span(node);
312
313        let id = *next_id;
314        *next_id += 1;
315
316        let mut unit = RawCodeUnit::new(
317            CodeUnitType::Import,
318            Language::CSharp,
319            text,
320            file_path.to_path_buf(),
321            span,
322        );
323        unit.temp_id = id;
324        unit.qualified_name = cs_qname(parent_qname, "using");
325
326        Some(unit)
327    }
328}
329
330impl LanguageParser for CSharpParser {
331    fn extract_units(
332        &self,
333        tree: &tree_sitter::Tree,
334        source: &str,
335        file_path: &Path,
336    ) -> AcbResult<Vec<RawCodeUnit>> {
337        let mut units = Vec::new();
338        let mut next_id = 0u64;
339
340        let module_name = file_path
341            .file_stem()
342            .and_then(|s| s.to_str())
343            .unwrap_or("unknown")
344            .to_string();
345
346        let root_span = node_to_span(tree.root_node());
347        let mut module_unit = RawCodeUnit::new(
348            CodeUnitType::Module,
349            Language::CSharp,
350            module_name.clone(),
351            file_path.to_path_buf(),
352            root_span,
353        );
354        module_unit.temp_id = next_id;
355        module_unit.qualified_name = module_name.clone();
356        next_id += 1;
357        units.push(module_unit);
358
359        self.extract_from_node(
360            tree.root_node(),
361            source,
362            file_path,
363            &mut units,
364            &mut next_id,
365            &module_name,
366        );
367
368        Ok(units)
369    }
370
371    fn is_test_file(&self, path: &Path, _source: &str) -> bool {
372        let name = path
373            .file_name()
374            .and_then(|n| n.to_str())
375            .unwrap_or("");
376        name.ends_with("Tests.cs")
377            || name.ends_with("Test.cs")
378            || name.starts_with("Test")
379    }
380}
381
382fn cs_qname(parent: &str, name: &str) -> String {
383    if parent.is_empty() {
384        name.to_string()
385    } else {
386        format!("{}.{}", parent, name)
387    }
388}
389
390/// Extract visibility from C# modifiers.
391fn extract_cs_visibility(node: tree_sitter::Node, source: &str) -> Visibility {
392    let mut cursor = node.walk();
393    for child in node.children(&mut cursor) {
394        if child.kind() == "modifier" {
395            let text = get_node_text(child, source);
396            match text {
397                "public" => return Visibility::Public,
398                "private" => return Visibility::Private,
399                "protected" | "internal" => return Visibility::Public,
400                _ => {}
401            }
402        }
403    }
404    Visibility::Private // C# default is private for members
405}
406
407/// Check if a member has a specific C# attribute.
408fn has_cs_attribute(node: tree_sitter::Node, source: &str, attribute: &str) -> bool {
409    let mut cursor = node.walk();
410    for child in node.children(&mut cursor) {
411        if child.kind() == "attribute_list" {
412            let text = get_node_text(child, source);
413            if text.contains(attribute) {
414                return true;
415            }
416        }
417    }
418    false
419}
420
421/// Check if a node's modifiers contain a specific keyword.
422fn node_text_contains_modifier(node: tree_sitter::Node, source: &str, keyword: &str) -> bool {
423    let mut cursor = node.walk();
424    for child in node.children(&mut cursor) {
425        if child.kind() == "modifier" && get_node_text(child, source) == keyword {
426            return true;
427        }
428    }
429    false
430}